FlatCAMGerber.py 77 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587
  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. # ##########################################################
  9. # File modified by: Marius Stanciu #
  10. # ##########################################################
  11. # ##########################################################
  12. # File Modified : Marcos Dumay de Medeiros #
  13. # Modifications under GPLv3 #
  14. # ##########################################################
  15. from shapely.geometry import Point, MultiLineString, LineString, LinearRing
  16. from appParsers.ParseGerber import Gerber
  17. from appObjects.FlatCAMObj import *
  18. import numpy as np
  19. from copy import deepcopy
  20. import gettext
  21. import appTranslation as fcTranslate
  22. import builtins
  23. fcTranslate.apply_language('strings')
  24. if '_' not in builtins.__dict__:
  25. _ = gettext.gettext
  26. class GerberObject(FlatCAMObj, Gerber):
  27. """
  28. Represents Gerber code.
  29. """
  30. optionChanged = QtCore.pyqtSignal(str)
  31. replotApertures = QtCore.pyqtSignal()
  32. do_buffer_signal = QtCore.pyqtSignal()
  33. ui_type = GerberObjectUI
  34. def __init__(self, name):
  35. self.decimals = self.app.decimals
  36. self.circle_steps = int(self.app.defaults["gerber_circle_steps"])
  37. Gerber.__init__(self, steps_per_circle=self.circle_steps)
  38. FlatCAMObj.__init__(self, name)
  39. self.kind = "gerber"
  40. # The 'name' is already in self.options from FlatCAMObj
  41. # Automatically updates the UI
  42. self.options.update({
  43. "plot": True,
  44. "multicolored": False,
  45. "solid": False,
  46. "noncoppermargin": 0.0,
  47. "noncopperrounded": False,
  48. "bboxmargin": 0.0,
  49. "bboxrounded": False,
  50. "aperture_display": False,
  51. "follow": False,
  52. "milling_type": 'cl',
  53. })
  54. # type of isolation: 0 = exteriors, 1 = interiors, 2 = complete isolation (both interiors and exteriors)
  55. self.iso_type = 2
  56. self.multigeo = False
  57. self.follow = False
  58. self.apertures_row = 0
  59. # store the source file here
  60. self.source_file = ""
  61. # list of rows with apertures plotted
  62. self.marked_rows = []
  63. # Mouse events
  64. self.mr = None
  65. self.mm = None
  66. self.mp = None
  67. # dict to store the polygons selected for isolation; key is the shape added to be plotted and value is the poly
  68. self.poly_dict = {}
  69. # store the status of grid snapping
  70. self.grid_status_memory = None
  71. self.units_found = self.app.defaults['units']
  72. self.fill_color = self.app.defaults['gerber_plot_fill']
  73. self.outline_color = self.app.defaults['gerber_plot_line']
  74. self.alpha_level = 'bf'
  75. # keep track if the UI is built so we don't have to build it every time
  76. self.ui_build = False
  77. # aperture marking storage
  78. self.mark_shapes_storage = {}
  79. # Attributes to be included in serialization
  80. # Always append to it because it carries contents
  81. # from predecessors.
  82. self.ser_attrs += ['options', 'kind', 'fill_color', 'outline_color', 'alpha_level']
  83. def set_ui(self, ui):
  84. """
  85. Maps options with GUI inputs.
  86. Connects GUI events to methods.
  87. :param ui: GUI object.
  88. :type ui: GerberObjectUI
  89. :return: None
  90. """
  91. FlatCAMObj.set_ui(self, ui)
  92. log.debug("GerberObject.set_ui()")
  93. self.units = self.app.defaults['units'].upper()
  94. self.replotApertures.connect(self.on_mark_cb_click_table)
  95. self.form_fields.update({
  96. "plot": self.ui.plot_cb,
  97. "multicolored": self.ui.multicolored_cb,
  98. "solid": self.ui.solid_cb,
  99. "noncoppermargin": self.ui.noncopper_margin_entry,
  100. "noncopperrounded": self.ui.noncopper_rounded_cb,
  101. "bboxmargin": self.ui.bbmargin_entry,
  102. "bboxrounded": self.ui.bbrounded_cb,
  103. "aperture_display": self.ui.aperture_table_visibility_cb,
  104. "follow": self.ui.follow_cb
  105. })
  106. # Fill form fields only on object create
  107. self.to_form()
  108. assert isinstance(self.ui, GerberObjectUI), \
  109. "Expected a GerberObjectUI, got %s" % type(self.ui)
  110. self.ui.plot_cb.stateChanged.connect(self.on_plot_cb_click)
  111. self.ui.solid_cb.stateChanged.connect(self.on_solid_cb_click)
  112. self.ui.multicolored_cb.stateChanged.connect(self.on_multicolored_cb_click)
  113. # Editor
  114. self.ui.editor_button.clicked.connect(lambda: self.app.object2editor())
  115. # Properties
  116. self.ui.properties_button.toggled.connect(self.on_properties)
  117. self.calculations_finished.connect(self.update_area_chull)
  118. # Tools
  119. self.ui.iso_button.clicked.connect(self.app.isolation_tool.run)
  120. self.ui.generate_ncc_button.clicked.connect(self.app.ncclear_tool.run)
  121. self.ui.generate_cutout_button.clicked.connect(self.app.cutout_tool.run)
  122. # Utilties
  123. self.ui.generate_bb_button.clicked.connect(self.on_generatebb_button_click)
  124. self.ui.generate_noncopper_button.clicked.connect(self.on_generatenoncopper_button_click)
  125. self.ui.util_button.clicked.connect(lambda st: self.ui.util_frame.show() if st else self.ui.util_frame.hide())
  126. self.ui.aperture_table_visibility_cb.stateChanged.connect(self.on_aperture_table_visibility_change)
  127. self.ui.follow_cb.stateChanged.connect(self.on_follow_cb_click)
  128. self.do_buffer_signal.connect(self.on_generate_buffer)
  129. # Show/Hide Advanced Options
  130. if self.app.defaults["global_app_level"] == 'b':
  131. self.ui.level.setText('<span style="color:green;"><b>%s</b></span>' % _('Basic'))
  132. self.ui.apertures_table_label.hide()
  133. self.ui.aperture_table_visibility_cb.hide()
  134. self.ui.follow_cb.hide()
  135. else:
  136. self.ui.level.setText('<span style="color:red;"><b>%s</b></span>' % _('Advanced'))
  137. if self.app.defaults["gerber_buffering"] == 'no':
  138. self.ui.create_buffer_button.show()
  139. try:
  140. self.ui.create_buffer_button.clicked.disconnect(self.on_generate_buffer)
  141. except TypeError:
  142. pass
  143. self.ui.create_buffer_button.clicked.connect(self.on_generate_buffer)
  144. else:
  145. self.ui.create_buffer_button.hide()
  146. # set initial state of the aperture table and associated widgets
  147. self.on_aperture_table_visibility_change()
  148. self.build_ui()
  149. self.units_found = self.app.defaults['units']
  150. def build_ui(self):
  151. FlatCAMObj.build_ui(self)
  152. if self.ui.aperture_table_visibility_cb.get_value() and self.ui_build is False:
  153. self.ui_build = True
  154. try:
  155. # if connected, disconnect the signal from the slot on item_changed as it creates issues
  156. self.ui.apertures_table.itemChanged.disconnect()
  157. except (TypeError, AttributeError):
  158. pass
  159. self.apertures_row = 0
  160. sort = []
  161. for k in list(self.apertures.keys()):
  162. sort.append(int(k))
  163. sorted_apertures = sorted(sort)
  164. n = len(sorted_apertures)
  165. self.ui.apertures_table.setRowCount(n)
  166. for ap_code in sorted_apertures:
  167. ap_code = str(ap_code)
  168. ap_id_item = QtWidgets.QTableWidgetItem('%d' % int(self.apertures_row + 1))
  169. ap_id_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
  170. self.ui.apertures_table.setItem(self.apertures_row, 0, ap_id_item) # Tool name/id
  171. ap_code_item = QtWidgets.QTableWidgetItem(ap_code)
  172. ap_code_item.setFlags(QtCore.Qt.ItemIsEnabled)
  173. ap_type_item = QtWidgets.QTableWidgetItem(str(self.apertures[ap_code]['type']))
  174. ap_type_item.setFlags(QtCore.Qt.ItemIsEnabled)
  175. if str(self.apertures[ap_code]['type']) == 'R' or str(self.apertures[ap_code]['type']) == 'O':
  176. ap_dim_item = QtWidgets.QTableWidgetItem(
  177. '%.*f, %.*f' % (self.decimals, self.apertures[ap_code]['width'],
  178. self.decimals, self.apertures[ap_code]['height']
  179. )
  180. )
  181. ap_dim_item.setFlags(QtCore.Qt.ItemIsEnabled)
  182. elif str(self.apertures[ap_code]['type']) == 'P':
  183. ap_dim_item = QtWidgets.QTableWidgetItem(
  184. '%.*f, %.*f' % (self.decimals, self.apertures[ap_code]['diam'],
  185. self.decimals, self.apertures[ap_code]['nVertices'])
  186. )
  187. ap_dim_item.setFlags(QtCore.Qt.ItemIsEnabled)
  188. else:
  189. ap_dim_item = QtWidgets.QTableWidgetItem('')
  190. ap_dim_item.setFlags(QtCore.Qt.ItemIsEnabled)
  191. try:
  192. if self.apertures[ap_code]['size'] is not None:
  193. ap_size_item = QtWidgets.QTableWidgetItem(
  194. '%.*f' % (self.decimals, float(self.apertures[ap_code]['size'])))
  195. else:
  196. ap_size_item = QtWidgets.QTableWidgetItem('')
  197. except KeyError:
  198. ap_size_item = QtWidgets.QTableWidgetItem('')
  199. ap_size_item.setFlags(QtCore.Qt.ItemIsEnabled)
  200. mark_item = FCCheckBox()
  201. mark_item.setLayoutDirection(QtCore.Qt.RightToLeft)
  202. # if self.ui.aperture_table_visibility_cb.isChecked():
  203. # mark_item.setChecked(True)
  204. self.ui.apertures_table.setItem(self.apertures_row, 1, ap_code_item) # Aperture Code
  205. self.ui.apertures_table.setItem(self.apertures_row, 2, ap_type_item) # Aperture Type
  206. self.ui.apertures_table.setItem(self.apertures_row, 3, ap_size_item) # Aperture Dimensions
  207. self.ui.apertures_table.setItem(self.apertures_row, 4, ap_dim_item) # Aperture Dimensions
  208. empty_plot_item = QtWidgets.QTableWidgetItem('')
  209. empty_plot_item.setFlags(~QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
  210. self.ui.apertures_table.setItem(self.apertures_row, 5, empty_plot_item)
  211. self.ui.apertures_table.setCellWidget(self.apertures_row, 5, mark_item)
  212. self.apertures_row += 1
  213. self.ui.apertures_table.selectColumn(0)
  214. self.ui.apertures_table.resizeColumnsToContents()
  215. self.ui.apertures_table.resizeRowsToContents()
  216. vertical_header = self.ui.apertures_table.verticalHeader()
  217. # vertical_header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
  218. vertical_header.hide()
  219. self.ui.apertures_table.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
  220. horizontal_header = self.ui.apertures_table.horizontalHeader()
  221. horizontal_header.setMinimumSectionSize(10)
  222. horizontal_header.setDefaultSectionSize(70)
  223. horizontal_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed)
  224. horizontal_header.resizeSection(0, 27)
  225. horizontal_header.setSectionResizeMode(1, QtWidgets.QHeaderView.ResizeToContents)
  226. horizontal_header.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeToContents)
  227. horizontal_header.setSectionResizeMode(3, QtWidgets.QHeaderView.ResizeToContents)
  228. horizontal_header.setSectionResizeMode(4, QtWidgets.QHeaderView.Stretch)
  229. horizontal_header.setSectionResizeMode(5, QtWidgets.QHeaderView.Fixed)
  230. horizontal_header.resizeSection(5, 17)
  231. self.ui.apertures_table.setColumnWidth(5, 17)
  232. self.ui.apertures_table.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
  233. self.ui.apertures_table.setSortingEnabled(False)
  234. self.ui.apertures_table.setMinimumHeight(self.ui.apertures_table.getHeight())
  235. self.ui.apertures_table.setMaximumHeight(self.ui.apertures_table.getHeight())
  236. # update the 'mark' checkboxes state according with what is stored in the self.marked_rows list
  237. if self.marked_rows:
  238. for row in range(self.ui.apertures_table.rowCount()):
  239. try:
  240. self.ui.apertures_table.cellWidget(row, 5).set_value(self.marked_rows[row])
  241. except IndexError:
  242. pass
  243. self.ui_connect()
  244. def ui_connect(self):
  245. for row in range(self.ui.apertures_table.rowCount()):
  246. try:
  247. self.ui.apertures_table.cellWidget(row, 5).clicked.disconnect(self.on_mark_cb_click_table)
  248. except (TypeError, AttributeError):
  249. pass
  250. self.ui.apertures_table.cellWidget(row, 5).clicked.connect(self.on_mark_cb_click_table)
  251. try:
  252. self.ui.mark_all_cb.clicked.disconnect(self.on_mark_all_click)
  253. except (TypeError, AttributeError):
  254. pass
  255. self.ui.mark_all_cb.clicked.connect(self.on_mark_all_click)
  256. def ui_disconnect(self):
  257. for row in range(self.ui.apertures_table.rowCount()):
  258. try:
  259. self.ui.apertures_table.cellWidget(row, 5).clicked.disconnect()
  260. except (TypeError, AttributeError):
  261. pass
  262. try:
  263. self.ui.mark_all_cb.clicked.disconnect(self.on_mark_all_click)
  264. except (TypeError, AttributeError):
  265. pass
  266. @staticmethod
  267. def buffer_handler(geo):
  268. new_geo = geo
  269. if isinstance(new_geo, list):
  270. new_geo = MultiPolygon(new_geo)
  271. new_geo = new_geo.buffer(0.0000001)
  272. new_geo = new_geo.buffer(-0.0000001)
  273. return new_geo
  274. def on_properties(self, state):
  275. if state:
  276. self.ui.properties_frame.show()
  277. else:
  278. self.ui.properties_frame.hide()
  279. return
  280. self.ui.treeWidget.clear()
  281. self.add_properties_items(obj=self, treeWidget=self.ui.treeWidget)
  282. # make sure that the FCTree widget columns are resized to content
  283. self.ui.treeWidget.resize_sig.emit()
  284. def on_generate_buffer(self):
  285. self.app.inform.emit('[WARNING_NOTCL] %s...' % _("Buffering solid geometry"))
  286. def buffer_task():
  287. with self.app.proc_container.new('%s ...' % _("Buffering")):
  288. output = self.app.pool.apply_async(self.buffer_handler, args=([self.solid_geometry]))
  289. self.solid_geometry = output.get()
  290. self.app.inform.emit('[success] %s' % _("Done."))
  291. self.plot_single_object.emit()
  292. self.app.worker_task.emit({'fcn': buffer_task, 'params': []})
  293. def on_generatenoncopper_button_click(self, *args):
  294. self.app.defaults.report_usage("gerber_on_generatenoncopper_button")
  295. self.read_form()
  296. name = self.options["name"] + "_noncopper"
  297. def geo_init(geo_obj, app_obj):
  298. assert geo_obj.kind == 'geometry', "Expected a Geometry object got %s" % type(geo_obj)
  299. if isinstance(self.solid_geometry, list):
  300. try:
  301. self.solid_geometry = MultiPolygon(self.solid_geometry)
  302. except Exception:
  303. self.solid_geometry = unary_union(self.solid_geometry)
  304. bounding_box = self.solid_geometry.envelope.buffer(float(self.options["noncoppermargin"]))
  305. if not self.options["noncopperrounded"]:
  306. bounding_box = bounding_box.envelope
  307. non_copper = bounding_box.difference(self.solid_geometry)
  308. if non_copper is None or non_copper.is_empty:
  309. app_obj.inform.emit("[ERROR_NOTCL] %s" % _("Operation could not be done."))
  310. return "fail"
  311. geo_obj.solid_geometry = non_copper
  312. self.app.app_obj.new_object("geometry", name, geo_init)
  313. def on_generatebb_button_click(self, *args):
  314. self.app.defaults.report_usage("gerber_on_generatebb_button")
  315. self.read_form()
  316. name = self.options["name"] + "_bbox"
  317. def geo_init(geo_obj, app_obj):
  318. assert geo_obj.kind == 'geometry', "Expected a Geometry object got %s" % type(geo_obj)
  319. if isinstance(self.solid_geometry, list):
  320. try:
  321. self.solid_geometry = MultiPolygon(self.solid_geometry)
  322. except Exception:
  323. self.solid_geometry = unary_union(self.solid_geometry)
  324. # Bounding box with rounded corners
  325. bounding_box = self.solid_geometry.envelope.buffer(float(self.options["bboxmargin"]))
  326. if not self.options["bboxrounded"]: # Remove rounded corners
  327. bounding_box = bounding_box.envelope
  328. if bounding_box is None or bounding_box.is_empty:
  329. app_obj.inform.emit("[ERROR_NOTCL] %s" % _("Operation could not be done."))
  330. return "fail"
  331. geo_obj.solid_geometry = bounding_box
  332. self.app.app_obj.new_object("geometry", name, geo_init)
  333. def isolate(self, iso_type=None, geometry=None, dia=None, passes=None, overlap=None, outname=None, combine=None,
  334. milling_type=None, follow=None, plot=True):
  335. """
  336. Creates an isolation routing geometry object in the project.
  337. :param iso_type: type of isolation to be done: 0 = exteriors, 1 = interiors and 2 = both
  338. :param geometry: specific geometry to isolate
  339. :param dia: Tool diameter
  340. :param passes: Number of tool widths to cut
  341. :param overlap: Overlap between passes in fraction of tool diameter
  342. :param outname: Base name of the output object
  343. :param combine: Boolean: if to combine passes in one resulting object in case of multiple passes
  344. :param milling_type: type of milling: conventional or climbing
  345. :param follow: Boolean: if to generate a 'follow' geometry
  346. :param plot: Boolean: if to plot the resulting geometry object
  347. :return: None
  348. """
  349. if geometry is None:
  350. work_geo = self.follow_geometry if follow is True else self.solid_geometry
  351. else:
  352. work_geo = geometry
  353. if dia is None:
  354. dia = float(self.app.defaults["tools_iso_tooldia"])
  355. if passes is None:
  356. passes = int(self.app.defaults["tools_iso_passes"])
  357. if overlap is None:
  358. overlap = float(self.app.defaults["tools_iso_overlap"])
  359. overlap /= 100.0
  360. combine = self.app.defaults["tools_iso_combine_passes"] if combine is None else bool(combine)
  361. if milling_type is None:
  362. milling_type = self.app.defaults["tools_iso_milling_type"]
  363. if iso_type is None:
  364. iso_t = 2
  365. else:
  366. iso_t = iso_type
  367. base_name = self.options["name"]
  368. if combine:
  369. if outname is None:
  370. if self.iso_type == 0:
  371. iso_name = base_name + "_ext_iso"
  372. elif self.iso_type == 1:
  373. iso_name = base_name + "_int_iso"
  374. else:
  375. iso_name = base_name + "_iso"
  376. else:
  377. iso_name = outname
  378. def iso_init(geo_obj, app_obj):
  379. # Propagate options
  380. geo_obj.options["cnctooldia"] = str(dia)
  381. geo_obj.tool_type = self.app.defaults["tools_iso_tool_type"]
  382. geo_obj.solid_geometry = []
  383. # transfer the Cut Z and Vtip and Vangle values in case that we use the V-Shape tool in Gerber UI
  384. if geo_obj.tool_type.lower() == 'v':
  385. new_cutz = self.app.defaults["tools_iso_tool_cutz"]
  386. new_vtipdia = self.app.defaults["tools_iso_tool_vtipdia"]
  387. new_vtipangle = self.app.defaults["tools_iso_tool_vtipangle"]
  388. tool_type = 'V'
  389. else:
  390. new_cutz = self.app.defaults['geometry_cutz']
  391. new_vtipdia = self.app.defaults['geometry_vtipdia']
  392. new_vtipangle = self.app.defaults['geometry_vtipangle']
  393. tool_type = 'C1'
  394. # store here the default data for Geometry Data
  395. default_data = {}
  396. default_data.update({
  397. "name": iso_name,
  398. "plot": self.app.defaults['geometry_plot'],
  399. "cutz": new_cutz,
  400. "vtipdia": new_vtipdia,
  401. "vtipangle": new_vtipangle,
  402. "travelz": self.app.defaults['geometry_travelz'],
  403. "feedrate": self.app.defaults['geometry_feedrate'],
  404. "feedrate_z": self.app.defaults['geometry_feedrate_z'],
  405. "feedrate_rapid": self.app.defaults['geometry_feedrate_rapid'],
  406. "dwell": self.app.defaults['geometry_dwell'],
  407. "dwelltime": self.app.defaults['geometry_dwelltime'],
  408. "multidepth": self.app.defaults['geometry_multidepth'],
  409. "ppname_g": self.app.defaults['geometry_ppname_g'],
  410. "depthperpass": self.app.defaults['geometry_depthperpass'],
  411. "extracut": self.app.defaults['geometry_extracut'],
  412. "extracut_length": self.app.defaults['geometry_extracut_length'],
  413. "toolchange": self.app.defaults['geometry_toolchange'],
  414. "toolchangez": self.app.defaults['geometry_toolchangez'],
  415. "endz": self.app.defaults['geometry_endz'],
  416. "spindlespeed": self.app.defaults['geometry_spindlespeed'],
  417. "toolchangexy": self.app.defaults['geometry_toolchangexy'],
  418. "startz": self.app.defaults['geometry_startz']
  419. })
  420. geo_obj.tools = {'1': {}}
  421. geo_obj.tools.update({
  422. '1': {
  423. 'tooldia': dia,
  424. 'offset': 'Path',
  425. 'offset_value': 0.0,
  426. 'type': 'Rough',
  427. 'tool_type': tool_type,
  428. 'data': default_data,
  429. 'solid_geometry': geo_obj.solid_geometry
  430. }
  431. })
  432. for nr_pass in range(passes):
  433. iso_offset = dia * ((2 * nr_pass + 1) / 2.0) - (nr_pass * overlap * dia)
  434. # if milling type is climb then the move is counter-clockwise around features
  435. mill_dir = 1 if milling_type == 'cl' else 0
  436. geom = self.generate_envelope(iso_offset, mill_dir, geometry=work_geo, env_iso_type=iso_t,
  437. follow=follow, nr_passes=nr_pass)
  438. if geom == 'fail':
  439. if plot:
  440. app_obj.inform.emit('[ERROR_NOTCL] %s' % _("Isolation geometry could not be generated."))
  441. return 'fail'
  442. geo_obj.solid_geometry.append(geom)
  443. # update the geometry in the tools
  444. geo_obj.tools['1']['solid_geometry'] = geo_obj.solid_geometry
  445. # detect if solid_geometry is empty and this require list flattening which is "heavy"
  446. # or just looking in the lists (they are one level depth) and if any is not empty
  447. # proceed with object creation, if there are empty and the number of them is the length
  448. # of the list then we have an empty solid_geometry which should raise a Custom Exception
  449. empty_cnt = 0
  450. if not isinstance(geo_obj.solid_geometry, list) and \
  451. not isinstance(geo_obj.solid_geometry, MultiPolygon):
  452. geo_obj.solid_geometry = [geo_obj.solid_geometry]
  453. for g in geo_obj.solid_geometry:
  454. if g:
  455. break
  456. else:
  457. empty_cnt += 1
  458. if empty_cnt == len(geo_obj.solid_geometry):
  459. raise ValidationError("Empty Geometry", None)
  460. else:
  461. if plot:
  462. app_obj.inform.emit('[success] %s: %s' %
  463. (_("Isolation geometry created"), geo_obj.options["name"]))
  464. # even if combine is checked, one pass is still single-geo
  465. geo_obj.multigeo = True if passes > 1 else False
  466. # ############################################################
  467. # ########## AREA SUBTRACTION ################################
  468. # ############################################################
  469. # if self.app.defaults["tools_iso_except"]:
  470. # self.app.proc_container.update_view_text(' %s' % _("Subtracting Geo"))
  471. # geo_obj.solid_geometry = self.area_subtraction(geo_obj.solid_geometry)
  472. self.app.app_obj.new_object("geometry", iso_name, iso_init, plot=plot)
  473. else:
  474. for i in range(passes):
  475. offset = dia * ((2 * i + 1) / 2.0) - (i * overlap * dia)
  476. if passes > 1:
  477. if outname is None:
  478. if self.iso_type == 0:
  479. iso_name = base_name + "_ext_iso" + str(i + 1)
  480. elif self.iso_type == 1:
  481. iso_name = base_name + "_int_iso" + str(i + 1)
  482. else:
  483. iso_name = base_name + "_iso" + str(i + 1)
  484. else:
  485. iso_name = outname
  486. else:
  487. if outname is None:
  488. if self.iso_type == 0:
  489. iso_name = base_name + "_ext_iso"
  490. elif self.iso_type == 1:
  491. iso_name = base_name + "_int_iso"
  492. else:
  493. iso_name = base_name + "_iso"
  494. else:
  495. iso_name = outname
  496. def iso_init(geo_obj, app_obj):
  497. # Propagate options
  498. geo_obj.options["cnctooldia"] = str(dia)
  499. geo_obj.tool_type = self.app.defaults["tools_iso_tool_type"]
  500. # if milling type is climb then the move is counter-clockwise around features
  501. mill_dir = 1 if milling_type == 'cl' else 0
  502. geom = self.generate_envelope(offset, mill_dir, geometry=work_geo, env_iso_type=iso_t,
  503. follow=follow, nr_passes=i)
  504. if geom == 'fail':
  505. if plot:
  506. app_obj.inform.emit('[ERROR_NOTCL] %s' % _("Isolation geometry could not be generated."))
  507. return 'fail'
  508. geo_obj.solid_geometry = geom
  509. # transfer the Cut Z and Vtip and VAngle values in case that we use the V-Shape tool in Gerber UI
  510. # even if the resulting geometry is not multigeo we add the tools dict which will hold the data
  511. # required to be transfered to the Geometry object
  512. if self.app.defaults["tools_iso_tool_type"].lower() == 'v':
  513. new_cutz = self.app.defaults["tools_iso_tool_cutz"]
  514. new_vtipdia = self.app.defaults["tools_iso_tool_vtipdia"]
  515. new_vtipangle = self.app.defaults["tools_iso_tool_vtipangle"]
  516. tool_type = 'V'
  517. else:
  518. new_cutz = self.app.defaults['geometry_cutz']
  519. new_vtipdia = self.app.defaults['geometry_vtipdia']
  520. new_vtipangle = self.app.defaults['geometry_vtipangle']
  521. tool_type = 'C1'
  522. # store here the default data for Geometry Data
  523. default_data = {}
  524. default_data.update({
  525. "name": iso_name,
  526. "plot": self.app.defaults['geometry_plot'],
  527. "cutz": new_cutz,
  528. "vtipdia": new_vtipdia,
  529. "vtipangle": new_vtipangle,
  530. "travelz": self.app.defaults['geometry_travelz'],
  531. "feedrate": self.app.defaults['geometry_feedrate'],
  532. "feedrate_z": self.app.defaults['geometry_feedrate_z'],
  533. "feedrate_rapid": self.app.defaults['geometry_feedrate_rapid'],
  534. "dwell": self.app.defaults['geometry_dwell'],
  535. "dwelltime": self.app.defaults['geometry_dwelltime'],
  536. "multidepth": self.app.defaults['geometry_multidepth'],
  537. "ppname_g": self.app.defaults['geometry_ppname_g'],
  538. "depthperpass": self.app.defaults['geometry_depthperpass'],
  539. "extracut": self.app.defaults['geometry_extracut'],
  540. "extracut_length": self.app.defaults['geometry_extracut_length'],
  541. "toolchange": self.app.defaults['geometry_toolchange'],
  542. "toolchangez": self.app.defaults['geometry_toolchangez'],
  543. "endz": self.app.defaults['geometry_endz'],
  544. "spindlespeed": self.app.defaults['geometry_spindlespeed'],
  545. "toolchangexy": self.app.defaults['geometry_toolchangexy'],
  546. "startz": self.app.defaults['geometry_startz']
  547. })
  548. geo_obj.tools = {'1': {}}
  549. geo_obj.tools.update({
  550. '1': {
  551. 'tooldia': dia,
  552. 'offset': 'Path',
  553. 'offset_value': 0.0,
  554. 'type': 'Rough',
  555. 'tool_type': tool_type,
  556. 'data': default_data,
  557. 'solid_geometry': geo_obj.solid_geometry
  558. }
  559. })
  560. # detect if solid_geometry is empty and this require list flattening which is "heavy"
  561. # or just looking in the lists (they are one level depth) and if any is not empty
  562. # proceed with object creation, if there are empty and the number of them is the length
  563. # of the list then we have an empty solid_geometry which should raise a Custom Exception
  564. empty_cnt = 0
  565. if not isinstance(geo_obj.solid_geometry, list):
  566. geo_obj.solid_geometry = [geo_obj.solid_geometry]
  567. for g in geo_obj.solid_geometry:
  568. if g:
  569. break
  570. else:
  571. empty_cnt += 1
  572. if empty_cnt == len(geo_obj.solid_geometry):
  573. raise ValidationError("Empty Geometry", None)
  574. else:
  575. if plot:
  576. app_obj.inform.emit('[success] %s: %s' %
  577. (_("Isolation geometry created"), geo_obj.options["name"]))
  578. geo_obj.multigeo = False
  579. # ############################################################
  580. # ########## AREA SUBTRACTION ################################
  581. # ############################################################
  582. # if self.app.defaults["tools_iso_except"]:
  583. # self.app.proc_container.update_view_text(' %s' % _("Subtracting Geo"))
  584. # geo_obj.solid_geometry = self.area_subtraction(geo_obj.solid_geometry)
  585. self.app.app_obj.new_object("geometry", iso_name, iso_init, plot=plot)
  586. def generate_envelope(self, offset, invert, geometry=None, env_iso_type=2, follow=None, nr_passes=0):
  587. # isolation_geometry produces an envelope that is going on the left of the geometry
  588. # (the copper features). To leave the least amount of burrs on the features
  589. # the tool needs to travel on the right side of the features (this is called conventional milling)
  590. # the first pass is the one cutting all of the features, so it needs to be reversed
  591. # the other passes overlap preceding ones and cut the left over copper. It is better for them
  592. # to cut on the right side of the left over copper i.e on the left side of the features.
  593. if follow:
  594. geom = self.isolation_geometry(offset, geometry=geometry, follow=follow)
  595. else:
  596. try:
  597. geom = self.isolation_geometry(offset, geometry=geometry, iso_type=env_iso_type, passes=nr_passes)
  598. except Exception as e:
  599. log.debug('GerberObject.isolate().generate_envelope() --> %s' % str(e))
  600. return 'fail'
  601. if invert:
  602. try:
  603. pl = []
  604. for p in geom:
  605. if p is not None:
  606. if isinstance(p, Polygon):
  607. pl.append(Polygon(p.exterior.coords[::-1], p.interiors))
  608. elif isinstance(p, LinearRing):
  609. pl.append(Polygon(p.coords[::-1]))
  610. geom = MultiPolygon(pl)
  611. except TypeError:
  612. if isinstance(geom, Polygon) and geom is not None:
  613. geom = Polygon(geom.exterior.coords[::-1], geom.interiors)
  614. elif isinstance(geom, LinearRing) and geom is not None:
  615. geom = Polygon(geom.coords[::-1])
  616. else:
  617. log.debug("GerberObject.isolate().generate_envelope() Error --> Unexpected Geometry %s" %
  618. type(geom))
  619. except Exception as e:
  620. log.debug("GerberObject.isolate().generate_envelope() Error --> %s" % str(e))
  621. return 'fail'
  622. return geom
  623. def follow_geo(self, outname=None):
  624. """
  625. Creates a geometry object "following" the gerber paths.
  626. :return: None
  627. """
  628. if outname is None:
  629. follow_name = self.options["name"] + "_follow"
  630. else:
  631. follow_name = outname
  632. def follow_init(follow_obj, app_obj):
  633. # Propagate options
  634. follow_obj.options["cnctooldia"] = str(self.app.defaults["tools_iso_tooldia"])
  635. follow_obj.solid_geometry = self.follow_geometry
  636. # TODO: Do something if this is None. Offer changing name?
  637. try:
  638. self.app.app_obj.new_object("geometry", follow_name, follow_init)
  639. except Exception as e:
  640. return "Operation failed: %s" % str(e)
  641. def on_plot_cb_click(self, *args):
  642. if self.muted_ui:
  643. return
  644. self.read_form_item('plot')
  645. self.plot()
  646. def on_solid_cb_click(self, *args):
  647. if self.muted_ui:
  648. return
  649. self.read_form_item('solid')
  650. self.plot()
  651. def on_multicolored_cb_click(self, *args):
  652. if self.muted_ui:
  653. return
  654. self.read_form_item('multicolored')
  655. self.plot()
  656. def on_follow_cb_click(self):
  657. if self.muted_ui:
  658. return
  659. self.plot()
  660. def on_aperture_table_visibility_change(self):
  661. if self.ui.aperture_table_visibility_cb.isChecked():
  662. # add the shapes storage for marking apertures
  663. for ap_code in self.apertures:
  664. self.mark_shapes_storage[ap_code] = []
  665. self.ui.apertures_table.setVisible(True)
  666. self.mark_shapes.enabled = True
  667. self.ui.mark_all_cb.setVisible(True)
  668. self.ui.mark_all_cb.setChecked(False)
  669. self.build_ui()
  670. else:
  671. self.ui.apertures_table.setVisible(False)
  672. self.ui.mark_all_cb.setVisible(False)
  673. # on hide disable all mark plots
  674. try:
  675. for row in range(self.ui.apertures_table.rowCount()):
  676. self.ui.apertures_table.cellWidget(row, 5).set_value(False)
  677. self.clear_plot_apertures()
  678. self.mark_shapes.enabled = False
  679. except Exception as e:
  680. log.debug(" GerberObject.on_aperture_visibility_changed() --> %s" % str(e))
  681. def convert_units(self, units):
  682. """
  683. Converts the units of the object by scaling dimensions in all geometry
  684. and options.
  685. :param units: Units to which to convert the object: "IN" or "MM".
  686. :type units: str
  687. :return: None
  688. :rtype: None
  689. """
  690. # units conversion to get a conversion should be done only once even if we found multiple
  691. # units declaration inside a Gerber file (it can happen to find also the obsolete declaration)
  692. if self.conversion_done is True:
  693. log.debug("Gerber units conversion cancelled. Already done.")
  694. return
  695. log.debug("FlatCAMObj.GerberObject.convert_units()")
  696. Gerber.convert_units(self, units)
  697. # self.options['isotooldia'] = float(self.options['isotooldia']) * factor
  698. # self.options['bboxmargin'] = float(self.options['bboxmargin']) * factor
  699. def plot(self, kind=None, **kwargs):
  700. """
  701. :param kind: Not used, for compatibility with the plot method for other objects
  702. :param kwargs: Color and face_color, visible
  703. :return:
  704. """
  705. log.debug(str(inspect.stack()[1][3]) + " --> GerberObject.plot()")
  706. # Does all the required setup and returns False
  707. # if the 'ptint' option is set to False.
  708. if not FlatCAMObj.plot(self):
  709. return
  710. if 'color' in kwargs:
  711. color = kwargs['color']
  712. else:
  713. color = self.outline_color
  714. if 'face_color' in kwargs:
  715. face_color = kwargs['face_color']
  716. else:
  717. face_color = self.fill_color
  718. if 'visible' not in kwargs:
  719. visible = self.options['plot']
  720. else:
  721. visible = kwargs['visible']
  722. # if the Follow Geometry checkbox is checked then plot only the follow geometry
  723. if self.ui.follow_cb.get_value():
  724. geometry = self.follow_geometry
  725. else:
  726. geometry = self.solid_geometry
  727. # Make sure geometry is iterable.
  728. try:
  729. __ = iter(geometry)
  730. except TypeError:
  731. geometry = [geometry]
  732. if self.app.is_legacy is False:
  733. def random_color():
  734. r_color = np.random.rand(4)
  735. r_color[3] = 1
  736. return r_color
  737. else:
  738. def random_color():
  739. while True:
  740. r_color = np.random.rand(4)
  741. r_color[3] = 1
  742. new_color = '#'
  743. for idx in range(len(r_color)):
  744. new_color += '%x' % int(r_color[idx] * 255)
  745. # do it until a valid color is generated
  746. # a valid color has the # symbol, another 6 chars for the color and the last 2 chars for alpha
  747. # for a total of 9 chars
  748. if len(new_color) == 9:
  749. break
  750. return new_color
  751. try:
  752. if self.options["solid"]:
  753. for g in geometry:
  754. if type(g) == Polygon or type(g) == LineString:
  755. self.add_shape(shape=g, color=color,
  756. face_color=random_color() if self.options['multicolored']
  757. else face_color, visible=visible)
  758. elif type(g) == Point:
  759. pass
  760. elif type(g) == MultiPolygon:
  761. for el in g.geoms:
  762. self.add_shape(shape=el, color=color,
  763. face_color=random_color() if self.options['multicolored']
  764. else face_color, visible=visible)
  765. else:
  766. try:
  767. for el in g:
  768. self.add_shape(shape=el, color=color,
  769. face_color=random_color() if self.options['multicolored']
  770. else face_color, visible=visible)
  771. except TypeError:
  772. self.add_shape(shape=g, color=color,
  773. face_color=random_color() if self.options['multicolored']
  774. else face_color, visible=visible)
  775. else:
  776. for g in geometry:
  777. if type(g) == Polygon or type(g) == LineString:
  778. self.add_shape(shape=g, color=random_color() if self.options['multicolored'] else 'black',
  779. visible=visible)
  780. elif type(g) == Point:
  781. pass
  782. elif type(g) == MultiPolygon:
  783. for el in g.geoms:
  784. self.add_shape(shape=el, color=random_color() if self.options['multicolored'] else 'black',
  785. visible=visible)
  786. else:
  787. for el in g:
  788. self.add_shape(shape=el, color=random_color() if self.options['multicolored'] else 'black',
  789. visible=visible)
  790. self.shapes.redraw(
  791. # update_colors=(self.fill_color, self.outline_color),
  792. # indexes=self.app.plotcanvas.shape_collection.data.keys()
  793. )
  794. except (ObjectDeleted, AttributeError):
  795. self.shapes.clear(update=True)
  796. except Exception as e:
  797. log.debug("GerberObject.plot() --> %s" % str(e))
  798. # experimental plot() when the solid_geometry is stored in the self.apertures
  799. def plot_aperture(self, only_flashes=False, run_thread=False, **kwargs):
  800. """
  801. :param only_flashes: plot only flashed
  802. :param run_thread: if True run the aperture plot as a thread in a worker
  803. :param kwargs: color and face_color
  804. :return:
  805. """
  806. log.debug(str(inspect.stack()[1][3]) + " --> GerberObject.plot_aperture()")
  807. # Does all the required setup and returns False
  808. # if the 'ptint' option is set to False.
  809. # if not FlatCAMObj.plot(self):
  810. # return
  811. # for marking apertures, line color and fill color are the same
  812. if 'color' in kwargs:
  813. color = kwargs['color']
  814. else:
  815. color = self.app.defaults['gerber_plot_fill']
  816. if 'marked_aperture' in kwargs:
  817. aperture_to_plot_mark = kwargs['marked_aperture']
  818. if aperture_to_plot_mark is None:
  819. return
  820. else:
  821. return
  822. if 'visible' not in kwargs:
  823. visibility = True
  824. else:
  825. visibility = kwargs['visible']
  826. def job_thread(app_obj):
  827. with self.app.proc_container.new('%s ...' % _("Plotting")):
  828. try:
  829. if aperture_to_plot_mark in self.apertures:
  830. for elem in app_obj.apertures[aperture_to_plot_mark]['geometry']:
  831. if 'solid' in elem:
  832. if only_flashes and not isinstance(elem['follow'], Point):
  833. continue
  834. geo = elem['solid']
  835. try:
  836. for el in geo:
  837. shape_key = app_obj.add_mark_shape(shape=el, color=color, face_color=color,
  838. visible=visibility)
  839. app_obj.mark_shapes_storage[aperture_to_plot_mark].append(shape_key)
  840. except TypeError:
  841. shape_key = app_obj.add_mark_shape(shape=geo, color=color, face_color=color,
  842. visible=visibility)
  843. app_obj.mark_shapes_storage[aperture_to_plot_mark].append(shape_key)
  844. app_obj.mark_shapes.redraw()
  845. except (ObjectDeleted, AttributeError):
  846. app_obj.clear_plot_apertures()
  847. except Exception as e:
  848. log.debug("GerberObject.plot_aperture() --> %s" % str(e))
  849. if run_thread:
  850. self.app.worker_task.emit({'fcn': job_thread, 'params': [self]})
  851. else:
  852. job_thread(self)
  853. def clear_plot_apertures(self, aperture='all'):
  854. """
  855. :param aperture: string; aperture for which to clear the mark shapes
  856. :return:
  857. """
  858. if self.mark_shapes_storage:
  859. if aperture == 'all':
  860. val = False if self.app.is_legacy is True else True
  861. self.mark_shapes.clear(update=val)
  862. else:
  863. for shape_key in self.mark_shapes_storage[aperture]:
  864. try:
  865. self.mark_shapes.remove(shape_key)
  866. except Exception as e:
  867. log.debug("GerberObject.clear_plot_apertures() -> %s" % str(e))
  868. self.mark_shapes_storage[aperture] = []
  869. self.mark_shapes.redraw()
  870. def clear_mark_all(self):
  871. self.ui.mark_all_cb.set_value(False)
  872. self.marked_rows[:] = []
  873. def on_mark_cb_click_table(self):
  874. """
  875. Will mark aperture geometries on canvas or delete the markings depending on the checkbox state
  876. :return:
  877. """
  878. self.ui_disconnect()
  879. try:
  880. cw = self.sender()
  881. cw_index = self.ui.apertures_table.indexAt(cw.pos())
  882. cw_row = cw_index.row()
  883. except AttributeError:
  884. cw_row = 0
  885. except TypeError:
  886. return
  887. self.marked_rows[:] = []
  888. try:
  889. aperture = self.ui.apertures_table.item(cw_row, 1).text()
  890. except AttributeError:
  891. self.ui_connect()
  892. return
  893. if self.ui.apertures_table.cellWidget(cw_row, 5).isChecked():
  894. self.marked_rows.append(True)
  895. # self.plot_aperture(color='#2d4606bf', marked_aperture=aperture, visible=True)
  896. self.plot_aperture(color=self.app.defaults['global_sel_draw_color'] + 'AF',
  897. marked_aperture=aperture, visible=True, run_thread=True)
  898. else:
  899. self.marked_rows.append(False)
  900. self.clear_plot_apertures(aperture=aperture)
  901. # make sure that the Mark All is disabled if one of the row mark's are disabled and
  902. # if all the row mark's are enabled also enable the Mark All checkbox
  903. cb_cnt = 0
  904. total_row = self.ui.apertures_table.rowCount()
  905. for row in range(total_row):
  906. if self.ui.apertures_table.cellWidget(row, 5).isChecked():
  907. cb_cnt += 1
  908. else:
  909. cb_cnt -= 1
  910. if cb_cnt < total_row:
  911. self.ui.mark_all_cb.setChecked(False)
  912. else:
  913. self.ui.mark_all_cb.setChecked(True)
  914. self.ui_connect()
  915. def on_mark_all_click(self):
  916. self.ui_disconnect()
  917. mark_all = self.ui.mark_all_cb.isChecked()
  918. for row in range(self.ui.apertures_table.rowCount()):
  919. # update the mark_rows list
  920. if mark_all:
  921. self.marked_rows.append(True)
  922. else:
  923. self.marked_rows[:] = []
  924. mark_cb = self.ui.apertures_table.cellWidget(row, 5)
  925. mark_cb.setChecked(mark_all)
  926. if mark_all:
  927. for aperture in self.apertures:
  928. # self.plot_aperture(color='#2d4606bf', marked_aperture=aperture, visible=True)
  929. self.plot_aperture(color=self.app.defaults['global_sel_draw_color'] + 'AF',
  930. marked_aperture=aperture, visible=True)
  931. # HACK: enable/disable the grid for a better look
  932. self.app.ui.grid_snap_btn.trigger()
  933. self.app.ui.grid_snap_btn.trigger()
  934. else:
  935. self.clear_plot_apertures()
  936. self.marked_rows[:] = []
  937. self.ui_connect()
  938. def export_gerber(self, whole, fract, g_zeros='L', factor=1):
  939. """
  940. Creates a Gerber file content to be exported to a file.
  941. :param whole: how many digits in the whole part of coordinates
  942. :param fract: how many decimals in coordinates
  943. :param g_zeros: type of the zero suppression used: LZ or TZ; string
  944. :param factor: factor to be applied onto the Gerber coordinates
  945. :return: Gerber_code
  946. """
  947. log.debug("GerberObject.export_gerber() --> Generating the Gerber code from the selected Gerber file")
  948. def tz_format(x, y, fac):
  949. x_c = x * fac
  950. y_c = y * fac
  951. x_form = "{:.{dec}f}".format(x_c, dec=fract)
  952. y_form = "{:.{dec}f}".format(y_c, dec=fract)
  953. # extract whole part and decimal part
  954. x_form = x_form.partition('.')
  955. y_form = y_form.partition('.')
  956. # left padd the 'whole' part with zeros
  957. x_whole = x_form[0].rjust(whole, '0')
  958. y_whole = y_form[0].rjust(whole, '0')
  959. # restore the coordinate padded in the left with 0 and added the decimal part
  960. # without the decinal dot
  961. x_form = x_whole + x_form[2]
  962. y_form = y_whole + y_form[2]
  963. return x_form, y_form
  964. def lz_format(x, y, fac):
  965. x_c = x * fac
  966. y_c = y * fac
  967. x_form = "{:.{dec}f}".format(x_c, dec=fract).replace('.', '')
  968. y_form = "{:.{dec}f}".format(y_c, dec=fract).replace('.', '')
  969. # pad with rear zeros
  970. x_form.ljust(length, '0')
  971. y_form.ljust(length, '0')
  972. return x_form, y_form
  973. # Gerber code is stored here
  974. gerber_code = ''
  975. # apertures processing
  976. try:
  977. length = whole + fract
  978. if '0' in self.apertures:
  979. if 'geometry' in self.apertures['0']:
  980. for geo_elem in self.apertures['0']['geometry']:
  981. if 'solid' in geo_elem:
  982. geo = geo_elem['solid']
  983. if not geo.is_empty and not isinstance(geo, LineString) and \
  984. not isinstance(geo, MultiLineString) and not isinstance(geo, Point):
  985. gerber_code += 'G36*\n'
  986. geo_coords = list(geo.exterior.coords)
  987. # first command is a move with pen-up D02 at the beginning of the geo
  988. if g_zeros == 'T':
  989. x_formatted, y_formatted = tz_format(geo_coords[0][0], geo_coords[0][1], factor)
  990. gerber_code += "X{xform}Y{yform}D02*\n".format(xform=x_formatted,
  991. yform=y_formatted)
  992. else:
  993. x_formatted, y_formatted = lz_format(geo_coords[0][0], geo_coords[0][1], factor)
  994. gerber_code += "X{xform}Y{yform}D02*\n".format(xform=x_formatted,
  995. yform=y_formatted)
  996. for coord in geo_coords[1:]:
  997. if g_zeros == 'T':
  998. x_formatted, y_formatted = tz_format(coord[0], coord[1], factor)
  999. gerber_code += "X{xform}Y{yform}D01*\n".format(xform=x_formatted,
  1000. yform=y_formatted)
  1001. else:
  1002. x_formatted, y_formatted = lz_format(coord[0], coord[1], factor)
  1003. gerber_code += "X{xform}Y{yform}D01*\n".format(xform=x_formatted,
  1004. yform=y_formatted)
  1005. gerber_code += 'D02*\n'
  1006. gerber_code += 'G37*\n'
  1007. clear_list = list(geo.interiors)
  1008. if clear_list:
  1009. gerber_code += '%LPC*%\n'
  1010. for clear_geo in clear_list:
  1011. gerber_code += 'G36*\n'
  1012. geo_coords = list(clear_geo.coords)
  1013. # first command is a move with pen-up D02 at the beginning of the geo
  1014. if g_zeros == 'T':
  1015. x_formatted, y_formatted = tz_format(
  1016. geo_coords[0][0], geo_coords[0][1], factor)
  1017. gerber_code += "X{xform}Y{yform}D02*\n".format(xform=x_formatted,
  1018. yform=y_formatted)
  1019. else:
  1020. x_formatted, y_formatted = lz_format(
  1021. geo_coords[0][0], geo_coords[0][1], factor)
  1022. gerber_code += "X{xform}Y{yform}D02*\n".format(xform=x_formatted,
  1023. yform=y_formatted)
  1024. prev_coord = geo_coords[0]
  1025. for coord in geo_coords[1:]:
  1026. if coord != prev_coord:
  1027. if g_zeros == 'T':
  1028. x_formatted, y_formatted = tz_format(coord[0], coord[1], factor)
  1029. gerber_code += "X{xform}Y{yform}D01*\n".format(xform=x_formatted,
  1030. yform=y_formatted)
  1031. else:
  1032. x_formatted, y_formatted = lz_format(coord[0], coord[1], factor)
  1033. gerber_code += "X{xform}Y{yform}D01*\n".format(xform=x_formatted,
  1034. yform=y_formatted)
  1035. prev_coord = coord
  1036. gerber_code += 'D02*\n'
  1037. gerber_code += 'G37*\n'
  1038. gerber_code += '%LPD*%\n'
  1039. elif isinstance(geo, LineString) or isinstance(geo, MultiLineString) or \
  1040. isinstance(geo, Point):
  1041. try:
  1042. if not geo.is_empty:
  1043. if isinstance(geo, Point):
  1044. if g_zeros == 'T':
  1045. x_formatted, y_formatted = tz_format(geo.x, geo.y, factor)
  1046. gerber_code += "X{xform}Y{yform}D03*\n".format(xform=x_formatted,
  1047. yform=y_formatted)
  1048. else:
  1049. x_formatted, y_formatted = lz_format(geo.x, geo.y, factor)
  1050. gerber_code += "X{xform}Y{yform}D03*\n".format(xform=x_formatted,
  1051. yform=y_formatted)
  1052. else:
  1053. geo_coords = list(geo.coords)
  1054. # first command is a move with pen-up D02 at the beginning of the geo
  1055. if g_zeros == 'T':
  1056. x_formatted, y_formatted = tz_format(
  1057. geo_coords[0][0], geo_coords[0][1], factor)
  1058. gerber_code += "X{xform}Y{yform}D02*\n".format(xform=x_formatted,
  1059. yform=y_formatted)
  1060. else:
  1061. x_formatted, y_formatted = lz_format(
  1062. geo_coords[0][0], geo_coords[0][1], factor)
  1063. gerber_code += "X{xform}Y{yform}D02*\n".format(xform=x_formatted,
  1064. yform=y_formatted)
  1065. prev_coord = geo_coords[0]
  1066. for coord in geo_coords[1:]:
  1067. if coord != prev_coord:
  1068. if g_zeros == 'T':
  1069. x_formatted, y_formatted = tz_format(coord[0], coord[1],
  1070. factor)
  1071. gerber_code += "X{xform}Y{yform}D01*\n".format(
  1072. xform=x_formatted,
  1073. yform=y_formatted)
  1074. else:
  1075. x_formatted, y_formatted = lz_format(coord[0], coord[1],
  1076. factor)
  1077. gerber_code += "X{xform}Y{yform}D01*\n".format(
  1078. xform=x_formatted,
  1079. yform=y_formatted)
  1080. prev_coord = coord
  1081. # gerber_code += "D02*\n"
  1082. except Exception as e:
  1083. log.debug("FlatCAMObj.GerberObject.export_gerber() 'follow' --> %s" % str(e))
  1084. if 'clear' in geo_elem:
  1085. geo = geo_elem['clear']
  1086. if not geo.is_empty:
  1087. gerber_code += '%LPC*%\n'
  1088. gerber_code += 'G36*\n'
  1089. geo_coords = list(geo.exterior.coords)
  1090. # first command is a move with pen-up D02 at the beginning of the geo
  1091. if g_zeros == 'T':
  1092. x_formatted, y_formatted = tz_format(geo_coords[0][0], geo_coords[0][1], factor)
  1093. gerber_code += "X{xform}Y{yform}D02*\n".format(xform=x_formatted,
  1094. yform=y_formatted)
  1095. else:
  1096. x_formatted, y_formatted = lz_format(geo_coords[0][0], geo_coords[0][1], factor)
  1097. gerber_code += "X{xform}Y{yform}D02*\n".format(xform=x_formatted,
  1098. yform=y_formatted)
  1099. prev_coord = geo_coords[0]
  1100. for coord in geo_coords[1:]:
  1101. if coord != prev_coord:
  1102. if g_zeros == 'T':
  1103. x_formatted, y_formatted = tz_format(coord[0], coord[1], factor)
  1104. gerber_code += "X{xform}Y{yform}D01*\n".format(xform=x_formatted,
  1105. yform=y_formatted)
  1106. else:
  1107. x_formatted, y_formatted = lz_format(coord[0], coord[1], factor)
  1108. gerber_code += "X{xform}Y{yform}D01*\n".format(xform=x_formatted,
  1109. yform=y_formatted)
  1110. prev_coord = coord
  1111. gerber_code += 'D02*\n'
  1112. gerber_code += 'G37*\n'
  1113. gerber_code += '%LPD*%\n'
  1114. except Exception as e:
  1115. log.debug("FlatCAMObj.GerberObject.export_gerber() '0' aperture --> %s" % str(e))
  1116. for apid in self.apertures:
  1117. if apid == '0':
  1118. continue
  1119. else:
  1120. gerber_code += 'D%s*\n' % str(apid)
  1121. if 'geometry' in self.apertures[apid]:
  1122. for geo_elem in self.apertures[apid]['geometry']:
  1123. try:
  1124. if 'follow' in geo_elem:
  1125. geo = geo_elem['follow']
  1126. if not geo.is_empty:
  1127. if isinstance(geo, Point):
  1128. if g_zeros == 'T':
  1129. x_formatted, y_formatted = tz_format(geo.x, geo.y, factor)
  1130. gerber_code += "X{xform}Y{yform}D03*\n".format(xform=x_formatted,
  1131. yform=y_formatted)
  1132. else:
  1133. x_formatted, y_formatted = lz_format(geo.x, geo.y, factor)
  1134. gerber_code += "X{xform}Y{yform}D03*\n".format(xform=x_formatted,
  1135. yform=y_formatted)
  1136. else:
  1137. geo_coords = list(geo.coords)
  1138. # first command is a move with pen-up D02 at the beginning of the geo
  1139. if g_zeros == 'T':
  1140. x_formatted, y_formatted = tz_format(
  1141. geo_coords[0][0], geo_coords[0][1], factor)
  1142. gerber_code += "X{xform}Y{yform}D02*\n".format(xform=x_formatted,
  1143. yform=y_formatted)
  1144. else:
  1145. x_formatted, y_formatted = lz_format(
  1146. geo_coords[0][0], geo_coords[0][1], factor)
  1147. gerber_code += "X{xform}Y{yform}D02*\n".format(xform=x_formatted,
  1148. yform=y_formatted)
  1149. prev_coord = geo_coords[0]
  1150. for coord in geo_coords[1:]:
  1151. if coord != prev_coord:
  1152. if g_zeros == 'T':
  1153. x_formatted, y_formatted = tz_format(coord[0], coord[1], factor)
  1154. gerber_code += "X{xform}Y{yform}D01*\n".format(xform=x_formatted,
  1155. yform=y_formatted)
  1156. else:
  1157. x_formatted, y_formatted = lz_format(coord[0], coord[1], factor)
  1158. gerber_code += "X{xform}Y{yform}D01*\n".format(xform=x_formatted,
  1159. yform=y_formatted)
  1160. prev_coord = coord
  1161. # gerber_code += "D02*\n"
  1162. except Exception as e:
  1163. log.debug("FlatCAMObj.GerberObject.export_gerber() 'follow' --> %s" % str(e))
  1164. try:
  1165. if 'clear' in geo_elem:
  1166. gerber_code += '%LPC*%\n'
  1167. geo = geo_elem['clear']
  1168. if not geo.is_empty:
  1169. if isinstance(geo, Point):
  1170. if g_zeros == 'T':
  1171. x_formatted, y_formatted = tz_format(geo.x, geo.y, factor)
  1172. gerber_code += "X{xform}Y{yform}D03*\n".format(xform=x_formatted,
  1173. yform=y_formatted)
  1174. else:
  1175. x_formatted, y_formatted = lz_format(geo.x, geo.y, factor)
  1176. gerber_code += "X{xform}Y{yform}D03*\n".format(xform=x_formatted,
  1177. yform=y_formatted)
  1178. elif isinstance(geo, Polygon):
  1179. geo_coords = list(geo.exterior.coords)
  1180. # first command is a move with pen-up D02 at the beginning of the geo
  1181. if g_zeros == 'T':
  1182. x_formatted, y_formatted = tz_format(
  1183. geo_coords[0][0], geo_coords[0][1], factor)
  1184. gerber_code += "X{xform}Y{yform}D02*\n".format(xform=x_formatted,
  1185. yform=y_formatted)
  1186. else:
  1187. x_formatted, y_formatted = lz_format(
  1188. geo_coords[0][0], geo_coords[0][1], factor)
  1189. gerber_code += "X{xform}Y{yform}D02*\n".format(xform=x_formatted,
  1190. yform=y_formatted)
  1191. prev_coord = geo_coords[0]
  1192. for coord in geo_coords[1:]:
  1193. if coord != prev_coord:
  1194. if g_zeros == 'T':
  1195. x_formatted, y_formatted = tz_format(coord[0], coord[1], factor)
  1196. gerber_code += "X{xform}Y{yform}D01*\n".format(xform=x_formatted,
  1197. yform=y_formatted)
  1198. else:
  1199. x_formatted, y_formatted = lz_format(coord[0], coord[1], factor)
  1200. gerber_code += "X{xform}Y{yform}D01*\n".format(xform=x_formatted,
  1201. yform=y_formatted)
  1202. prev_coord = coord
  1203. for geo_int in geo.interiors:
  1204. geo_coords = list(geo_int.coords)
  1205. # first command is a move with pen-up D02 at the beginning of the geo
  1206. if g_zeros == 'T':
  1207. x_formatted, y_formatted = tz_format(
  1208. geo_coords[0][0], geo_coords[0][1], factor)
  1209. gerber_code += "X{xform}Y{yform}D02*\n".format(xform=x_formatted,
  1210. yform=y_formatted)
  1211. else:
  1212. x_formatted, y_formatted = lz_format(
  1213. geo_coords[0][0], geo_coords[0][1], factor)
  1214. gerber_code += "X{xform}Y{yform}D02*\n".format(xform=x_formatted,
  1215. yform=y_formatted)
  1216. prev_coord = geo_coords[0]
  1217. for coord in geo_coords[1:]:
  1218. if coord != prev_coord:
  1219. if g_zeros == 'T':
  1220. x_formatted, y_formatted = tz_format(coord[0], coord[1], factor)
  1221. gerber_code += "X{xform}Y{yform}D01*\n".format(
  1222. xform=x_formatted,
  1223. yform=y_formatted)
  1224. else:
  1225. x_formatted, y_formatted = lz_format(coord[0], coord[1], factor)
  1226. gerber_code += "X{xform}Y{yform}D01*\n".format(
  1227. xform=x_formatted,
  1228. yform=y_formatted)
  1229. prev_coord = coord
  1230. else:
  1231. geo_coords = list(geo.coords)
  1232. # first command is a move with pen-up D02 at the beginning of the geo
  1233. if g_zeros == 'T':
  1234. x_formatted, y_formatted = tz_format(
  1235. geo_coords[0][0], geo_coords[0][1], factor)
  1236. gerber_code += "X{xform}Y{yform}D02*\n".format(xform=x_formatted,
  1237. yform=y_formatted)
  1238. else:
  1239. x_formatted, y_formatted = lz_format(
  1240. geo_coords[0][0], geo_coords[0][1], factor)
  1241. gerber_code += "X{xform}Y{yform}D02*\n".format(xform=x_formatted,
  1242. yform=y_formatted)
  1243. prev_coord = geo_coords[0]
  1244. for coord in geo_coords[1:]:
  1245. if coord != prev_coord:
  1246. if g_zeros == 'T':
  1247. x_formatted, y_formatted = tz_format(coord[0], coord[1], factor)
  1248. gerber_code += "X{xform}Y{yform}D01*\n".format(xform=x_formatted,
  1249. yform=y_formatted)
  1250. else:
  1251. x_formatted, y_formatted = lz_format(coord[0], coord[1], factor)
  1252. gerber_code += "X{xform}Y{yform}D01*\n".format(xform=x_formatted,
  1253. yform=y_formatted)
  1254. prev_coord = coord
  1255. # gerber_code += "D02*\n"
  1256. gerber_code += '%LPD*%\n'
  1257. except Exception as e:
  1258. log.debug("FlatCAMObj.GerberObject.export_gerber() 'clear' --> %s" % str(e))
  1259. if not self.apertures:
  1260. log.debug("FlatCAMObj.GerberObject.export_gerber() --> Gerber Object is empty: no apertures.")
  1261. return 'fail'
  1262. return gerber_code
  1263. @staticmethod
  1264. def merge(grb_list, grb_final):
  1265. """
  1266. Merges the geometry of objects in geo_list into
  1267. the geometry of geo_final.
  1268. :param grb_list: List of GerberObject Objects to join.
  1269. :param grb_final: Destination GeometryObject object.
  1270. :return: None
  1271. """
  1272. if grb_final.solid_geometry is None:
  1273. grb_final.solid_geometry = []
  1274. grb_final.follow_geometry = []
  1275. if not grb_final.apertures:
  1276. grb_final.apertures = {}
  1277. if type(grb_final.solid_geometry) is not list:
  1278. grb_final.solid_geometry = [grb_final.solid_geometry]
  1279. grb_final.follow_geometry = [grb_final.follow_geometry]
  1280. for grb in grb_list:
  1281. # Expand lists
  1282. if type(grb) is list:
  1283. GerberObject.merge(grb_list=grb, grb_final=grb_final)
  1284. else: # If not list, just append
  1285. for option in grb.options:
  1286. if option != 'name':
  1287. try:
  1288. grb_final.options[option] = grb.options[option]
  1289. except KeyError:
  1290. log.warning("Failed to copy option.", option)
  1291. try:
  1292. for geos in grb.solid_geometry:
  1293. grb_final.solid_geometry.append(geos)
  1294. grb_final.follow_geometry.append(geos)
  1295. except TypeError:
  1296. grb_final.solid_geometry.append(grb.solid_geometry)
  1297. grb_final.follow_geometry.append(grb.solid_geometry)
  1298. for ap in grb.apertures:
  1299. if ap not in grb_final.apertures:
  1300. grb_final.apertures[ap] = grb.apertures[ap]
  1301. else:
  1302. # create a list of integers out of the grb.apertures keys and find the max of that value
  1303. # then, the aperture duplicate is assigned an id value incremented with 1,
  1304. # and finally made string because the apertures dict keys are strings
  1305. max_ap = str(max([int(k) for k in grb_final.apertures.keys()]) + 1)
  1306. grb_final.apertures[max_ap] = {}
  1307. grb_final.apertures[max_ap]['geometry'] = []
  1308. for k, v in grb.apertures[ap].items():
  1309. grb_final.apertures[max_ap][k] = deepcopy(v)
  1310. grb_final.solid_geometry = MultiPolygon(grb_final.solid_geometry)
  1311. grb_final.follow_geometry = MultiPolygon(grb_final.follow_geometry)
  1312. def mirror(self, axis, point):
  1313. Gerber.mirror(self, axis=axis, point=point)
  1314. self.replotApertures.emit()
  1315. def offset(self, vect):
  1316. Gerber.offset(self, vect=vect)
  1317. self.replotApertures.emit()
  1318. def rotate(self, angle, point):
  1319. Gerber.rotate(self, angle=angle, point=point)
  1320. self.replotApertures.emit()
  1321. def scale(self, xfactor, yfactor=None, point=None):
  1322. Gerber.scale(self, xfactor=xfactor, yfactor=yfactor, point=point)
  1323. self.replotApertures.emit()
  1324. def skew(self, angle_x, angle_y, point):
  1325. Gerber.skew(self, angle_x=angle_x, angle_y=angle_y, point=point)
  1326. self.replotApertures.emit()
  1327. def buffer(self, distance, join=2, factor=None):
  1328. Gerber.buffer(self, distance=distance, join=join, factor=factor)
  1329. self.replotApertures.emit()
  1330. def serialize(self):
  1331. return {
  1332. "options": self.options,
  1333. "kind": self.kind
  1334. }