ToolCutOut.py 48 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174
  1. # ##########################################################
  2. # FlatCAM: 2D Post-processing for Manufacturing #
  3. # File Author: Marius Adrian Stanciu (c) #
  4. # Date: 3/10/2019 #
  5. # MIT Licence #
  6. # ##########################################################
  7. from PyQt5 import QtWidgets, QtGui, QtCore
  8. from FlatCAMTool import FlatCAMTool
  9. from flatcamGUI.GUIElements import FCDoubleSpinner, FCCheckBox, RadioSet, FCComboBox
  10. from FlatCAMObj import FlatCAMGerber
  11. from shapely.geometry import box, MultiPolygon, Polygon, LineString, LinearRing
  12. from shapely.ops import cascaded_union, unary_union
  13. import shapely.affinity as affinity
  14. from matplotlib.backend_bases import KeyEvent as mpl_key_event
  15. from numpy import Inf
  16. from copy import deepcopy
  17. import math
  18. import logging
  19. import gettext
  20. import FlatCAMTranslation as fcTranslate
  21. import builtins
  22. fcTranslate.apply_language('strings')
  23. if '_' not in builtins.__dict__:
  24. _ = gettext.gettext
  25. log = logging.getLogger('base')
  26. class CutOut(FlatCAMTool):
  27. toolName = _("Cutout PCB")
  28. def __init__(self, app):
  29. FlatCAMTool.__init__(self, app)
  30. self.app = app
  31. self.canvas = app.plotcanvas
  32. self.decimals = 4
  33. # Title
  34. title_label = QtWidgets.QLabel("%s" % self.toolName)
  35. title_label.setStyleSheet("""
  36. QLabel
  37. {
  38. font-size: 16px;
  39. font-weight: bold;
  40. }
  41. """)
  42. self.layout.addWidget(title_label)
  43. # Form Layout
  44. form_layout = QtWidgets.QFormLayout()
  45. self.layout.addLayout(form_layout)
  46. # Type of object to be cutout
  47. self.type_obj_combo = QtWidgets.QComboBox()
  48. self.type_obj_combo.addItem("Gerber")
  49. self.type_obj_combo.addItem("Excellon")
  50. self.type_obj_combo.addItem("Geometry")
  51. # we get rid of item1 ("Excellon") as it is not suitable for creating film
  52. self.type_obj_combo.view().setRowHidden(1, True)
  53. self.type_obj_combo.setItemIcon(0, QtGui.QIcon("share/flatcam_icon16.png"))
  54. # self.type_obj_combo.setItemIcon(1, QtGui.QIcon("share/drill16.png"))
  55. self.type_obj_combo.setItemIcon(2, QtGui.QIcon("share/geometry16.png"))
  56. self.type_obj_combo_label = QtWidgets.QLabel('%s:' % _("Obj Type"))
  57. self.type_obj_combo_label.setToolTip(
  58. _("Specify the type of object to be cutout.\n"
  59. "It can be of type: Gerber or Geometry.\n"
  60. "What is selected here will dictate the kind\n"
  61. "of objects that will populate the 'Object' combobox.")
  62. )
  63. self.type_obj_combo_label.setMinimumWidth(60)
  64. form_layout.addRow(self.type_obj_combo_label, self.type_obj_combo)
  65. # Object to be cutout
  66. self.obj_combo = QtWidgets.QComboBox()
  67. self.obj_combo.setModel(self.app.collection)
  68. self.obj_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
  69. self.obj_combo.setCurrentIndex(1)
  70. self.object_label = QtWidgets.QLabel('%s:' % _("Object"))
  71. self.object_label.setToolTip(
  72. _("Object to be cutout. ")
  73. )
  74. form_layout.addRow(self.object_label, self.obj_combo)
  75. # Object kind
  76. self.kindlabel = QtWidgets.QLabel('%s:' % _('Obj kind'))
  77. self.kindlabel.setToolTip(
  78. _("Choice of what kind the object we want to cutout is.<BR>"
  79. "- <B>Single</B>: contain a single PCB Gerber outline object.<BR>"
  80. "- <B>Panel</B>: a panel PCB Gerber object, which is made\n"
  81. "out of many individual PCB outlines.")
  82. )
  83. self.obj_kind_combo = RadioSet([
  84. {"label": _("Single"), "value": "single"},
  85. {"label": _("Panel"), "value": "panel"},
  86. ])
  87. form_layout.addRow(self.kindlabel, self.obj_kind_combo)
  88. # Tool Diameter
  89. self.dia = FCDoubleSpinner()
  90. self.dia.set_precision(self.decimals)
  91. self.dia_label = QtWidgets.QLabel('%s:' % _("Tool dia"))
  92. self.dia_label.setToolTip(
  93. _("Diameter of the tool used to cutout\n"
  94. "the PCB shape out of the surrounding material.")
  95. )
  96. form_layout.addRow(self.dia_label, self.dia)
  97. # Margin
  98. self.margin = FCDoubleSpinner()
  99. self.margin.set_precision(self.decimals)
  100. self.margin_label = QtWidgets.QLabel('%s:' % _("Margin:"))
  101. self.margin_label.setToolTip(
  102. _("Margin over bounds. A positive value here\n"
  103. "will make the cutout of the PCB further from\n"
  104. "the actual PCB border")
  105. )
  106. form_layout.addRow(self.margin_label, self.margin)
  107. # Gapsize
  108. self.gapsize = FCDoubleSpinner()
  109. self.gapsize.set_precision(self.decimals)
  110. self.gapsize_label = QtWidgets.QLabel('%s:' % _("Gap size:"))
  111. self.gapsize_label.setToolTip(
  112. _("The size of the bridge gaps in the cutout\n"
  113. "used to keep the board connected to\n"
  114. "the surrounding material (the one \n"
  115. "from which the PCB is cutout).")
  116. )
  117. form_layout.addRow(self.gapsize_label, self.gapsize)
  118. # How gaps wil be rendered:
  119. # lr - left + right
  120. # tb - top + bottom
  121. # 4 - left + right +top + bottom
  122. # 2lr - 2*left + 2*right
  123. # 2tb - 2*top + 2*bottom
  124. # 8 - 2*left + 2*right +2*top + 2*bottom
  125. # Surrounding convex box shape
  126. self.convex_box = FCCheckBox()
  127. self.convex_box_label = QtWidgets.QLabel('%s:' % _("Convex Sh."))
  128. self.convex_box_label.setToolTip(
  129. _("Create a convex shape surrounding the entire PCB.\n"
  130. "Used only if the source object type is Gerber.")
  131. )
  132. form_layout.addRow(self.convex_box_label, self.convex_box)
  133. # Title2
  134. title_param_label = QtWidgets.QLabel("<font size=4><b>%s</b></font>" % _('A. Automatic Bridge Gaps'))
  135. title_param_label.setToolTip(
  136. _("This section handle creation of automatic bridge gaps.")
  137. )
  138. self.layout.addWidget(title_param_label)
  139. # Form Layout
  140. form_layout_2 = QtWidgets.QFormLayout()
  141. self.layout.addLayout(form_layout_2)
  142. # Gaps
  143. gaps_label = QtWidgets.QLabel('%s:' % _('Gaps'))
  144. gaps_label.setToolTip(
  145. _("Number of gaps used for the Automatic cutout.\n"
  146. "There can be maximum 8 bridges/gaps.\n"
  147. "The choices are:\n"
  148. "- None - no gaps\n"
  149. "- lr - left + right\n"
  150. "- tb - top + bottom\n"
  151. "- 4 - left + right +top + bottom\n"
  152. "- 2lr - 2*left + 2*right\n"
  153. "- 2tb - 2*top + 2*bottom\n"
  154. "- 8 - 2*left + 2*right +2*top + 2*bottom")
  155. )
  156. gaps_label.setMinimumWidth(60)
  157. self.gaps = FCComboBox()
  158. gaps_items = ['None', 'LR', 'TB', '4', '2LR', '2TB', '8']
  159. for it in gaps_items:
  160. self.gaps.addItem(it)
  161. self.gaps.setStyleSheet('background-color: rgb(255,255,255)')
  162. form_layout_2.addRow(gaps_label, self.gaps)
  163. # Buttons
  164. hlay = QtWidgets.QHBoxLayout()
  165. self.layout.addLayout(hlay)
  166. title_ff_label = QtWidgets.QLabel("<b>%s:</b>" % _('FreeForm'))
  167. title_ff_label.setToolTip(
  168. _("The cutout shape can be of ny shape.\n"
  169. "Useful when the PCB has a non-rectangular shape.")
  170. )
  171. hlay.addWidget(title_ff_label)
  172. hlay.addStretch()
  173. self.ff_cutout_object_btn = QtWidgets.QPushButton(_("Generate Geo"))
  174. self.ff_cutout_object_btn.setToolTip(
  175. _("Cutout the selected object.\n"
  176. "The cutout shape can be of any shape.\n"
  177. "Useful when the PCB has a non-rectangular shape.")
  178. )
  179. hlay.addWidget(self.ff_cutout_object_btn)
  180. hlay2 = QtWidgets.QHBoxLayout()
  181. self.layout.addLayout(hlay2)
  182. title_rct_label = QtWidgets.QLabel("<b>%s:</b>" % _('Rectangular'))
  183. title_rct_label.setToolTip(
  184. _("The resulting cutout shape is\n"
  185. "always a rectangle shape and it will be\n"
  186. "the bounding box of the Object.")
  187. )
  188. hlay2.addWidget(title_rct_label)
  189. hlay2.addStretch()
  190. self.rect_cutout_object_btn = QtWidgets.QPushButton(_("Generate Geo"))
  191. self.rect_cutout_object_btn.setToolTip(
  192. _("Cutout the selected object.\n"
  193. "The resulting cutout shape is\n"
  194. "always a rectangle shape and it will be\n"
  195. "the bounding box of the Object.")
  196. )
  197. hlay2.addWidget(self.rect_cutout_object_btn)
  198. # Title5
  199. title_manual_label = QtWidgets.QLabel("<font size=4><b>%s</b></font>" % _('B. Manual Bridge Gaps'))
  200. title_manual_label.setToolTip(
  201. _("This section handle creation of manual bridge gaps.\n"
  202. "This is done by mouse clicking on the perimeter of the\n"
  203. "Geometry object that is used as a cutout object. ")
  204. )
  205. self.layout.addWidget(title_manual_label)
  206. # Form Layout
  207. form_layout_3 = QtWidgets.QFormLayout()
  208. self.layout.addLayout(form_layout_3)
  209. # Manual Geo Object
  210. self.man_object_combo = QtWidgets.QComboBox()
  211. self.man_object_combo.setModel(self.app.collection)
  212. self.man_object_combo.setRootModelIndex(self.app.collection.index(2, 0, QtCore.QModelIndex()))
  213. self.man_object_combo.setCurrentIndex(1)
  214. self.man_object_label = QtWidgets.QLabel('%s:' % _("Geo Obj"))
  215. self.man_object_label.setToolTip(
  216. _("Geometry object used to create the manual cutout.")
  217. )
  218. self.man_object_label.setMinimumWidth(60)
  219. # e_lab_0 = QtWidgets.QLabel('')
  220. form_layout_3.addRow(self.man_object_label, self.man_object_combo)
  221. # form_layout_3.addRow(e_lab_0)
  222. hlay3 = QtWidgets.QHBoxLayout()
  223. self.layout.addLayout(hlay3)
  224. self.man_geo_label = QtWidgets.QLabel('%s:' % _("Manual Geo"))
  225. self.man_geo_label.setToolTip(
  226. _("If the object to be cutout is a Gerber\n"
  227. "first create a Geometry that surrounds it,\n"
  228. "to be used as the cutout, if one doesn't exist yet.\n"
  229. "Select the source Gerber file in the top object combobox.")
  230. )
  231. hlay3.addWidget(self.man_geo_label)
  232. hlay3.addStretch()
  233. self.man_geo_creation_btn = QtWidgets.QPushButton(_("Generate Geo"))
  234. self.man_geo_creation_btn.setToolTip(
  235. _("If the object to be cutout is a Gerber\n"
  236. "first create a Geometry that surrounds it,\n"
  237. "to be used as the cutout, if one doesn't exist yet.\n"
  238. "Select the source Gerber file in the top object combobox.")
  239. )
  240. hlay3.addWidget(self.man_geo_creation_btn)
  241. hlay4 = QtWidgets.QHBoxLayout()
  242. self.layout.addLayout(hlay4)
  243. self.man_bridge_gaps_label = QtWidgets.QLabel('%s:' % _("Manual Add Bridge Gaps"))
  244. self.man_bridge_gaps_label.setToolTip(
  245. _("Use the left mouse button (LMB) click\n"
  246. "to create a bridge gap to separate the PCB from\n"
  247. "the surrounding material.")
  248. )
  249. hlay4.addWidget(self.man_bridge_gaps_label)
  250. hlay4.addStretch()
  251. self.man_gaps_creation_btn = QtWidgets.QPushButton(_("Generate Gap"))
  252. self.man_gaps_creation_btn.setToolTip(
  253. _("Use the left mouse button (LMB) click\n"
  254. "to create a bridge gap to separate the PCB from\n"
  255. "the surrounding material.\n"
  256. "The LMB click has to be done on the perimeter of\n"
  257. "the Geometry object used as a cutout geometry.")
  258. )
  259. hlay4.addWidget(self.man_gaps_creation_btn)
  260. self.layout.addStretch()
  261. self.cutting_gapsize = 0.0
  262. self.cutting_dia = 0.0
  263. # true if we want to repeat the gap without clicking again on the button
  264. self.repeat_gap = False
  265. self.flat_geometry = []
  266. # this is the Geometry object generated in this class to be used for adding manual gaps
  267. self.man_cutout_obj = None
  268. # if mouse is dragging set the object True
  269. self.mouse_is_dragging = False
  270. # event handlers references
  271. self.kp = None
  272. self.mm = None
  273. self.mr = None
  274. # hold the mouse position here
  275. self.x_pos = None
  276. self.y_pos = None
  277. # Signals
  278. self.ff_cutout_object_btn.clicked.connect(self.on_freeform_cutout)
  279. self.rect_cutout_object_btn.clicked.connect(self.on_rectangular_cutout)
  280. self.type_obj_combo.currentIndexChanged.connect(self.on_type_obj_index_changed)
  281. self.man_geo_creation_btn.clicked.connect(self.on_manual_geo)
  282. self.man_gaps_creation_btn.clicked.connect(self.on_manual_gap_click)
  283. def on_type_obj_index_changed(self, index):
  284. obj_type = self.type_obj_combo.currentIndex()
  285. self.obj_combo.setRootModelIndex(self.app.collection.index(obj_type, 0, QtCore.QModelIndex()))
  286. self.obj_combo.setCurrentIndex(0)
  287. def run(self, toggle=True):
  288. self.app.report_usage("ToolCutOut()")
  289. if toggle:
  290. # if the splitter is hidden, display it, else hide it but only if the current widget is the same
  291. if self.app.ui.splitter.sizes()[0] == 0:
  292. self.app.ui.splitter.setSizes([1, 1])
  293. else:
  294. try:
  295. if self.app.ui.tool_scroll_area.widget().objectName() == self.toolName:
  296. # if tab is populated with the tool but it does not have the focus, focus on it
  297. if not self.app.ui.notebook.currentWidget() is self.app.ui.tool_tab:
  298. # focus on Tool Tab
  299. self.app.ui.notebook.setCurrentWidget(self.app.ui.tool_tab)
  300. else:
  301. self.app.ui.splitter.setSizes([0, 1])
  302. except AttributeError:
  303. pass
  304. else:
  305. if self.app.ui.splitter.sizes()[0] == 0:
  306. self.app.ui.splitter.setSizes([1, 1])
  307. FlatCAMTool.run(self)
  308. self.set_tool_ui()
  309. self.app.ui.notebook.setTabText(2, _("Cutout Tool"))
  310. def install(self, icon=None, separator=None, **kwargs):
  311. FlatCAMTool.install(self, icon, separator, shortcut='ALT+U', **kwargs)
  312. def set_tool_ui(self):
  313. self.reset_fields()
  314. self.dia.set_value(float(self.app.defaults["tools_cutouttooldia"]))
  315. self.obj_kind_combo.set_value(self.app.defaults["tools_cutoutkind"])
  316. self.margin.set_value(float(self.app.defaults["tools_cutoutmargin"]))
  317. self.gapsize.set_value(float(self.app.defaults["tools_cutoutgapsize"]))
  318. self.gaps.set_value(self.app.defaults["tools_gaps_ff"])
  319. self.convex_box.set_value(self.app.defaults['tools_cutout_convexshape'])
  320. def on_freeform_cutout(self):
  321. # def subtract_rectangle(obj_, x0, y0, x1, y1):
  322. # pts = [(x0, y0), (x1, y0), (x1, y1), (x0, y1)]
  323. # obj_.subtract_polygon(pts)
  324. name = self.obj_combo.currentText()
  325. # Get source object.
  326. try:
  327. cutout_obj = self.app.collection.get_by_name(str(name))
  328. except Exception as e:
  329. log.debug("CutOut.on_freeform_cutout() --> %s" % str(e))
  330. self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), name))
  331. return "Could not retrieve object: %s" % name
  332. if cutout_obj is None:
  333. self.app.inform.emit('[ERROR_NOTCL] %s' %
  334. _("There is no object selected for Cutout.\nSelect one and try again."))
  335. return
  336. dia = float(self.dia.get_value())
  337. if 0 in {dia}:
  338. self.app.inform.emit('[WARNING_NOTCL] %s' %
  339. _("Tool Diameter is zero value. Change it to a positive real number."))
  340. return "Tool Diameter is zero value. Change it to a positive real number."
  341. try:
  342. kind = self.obj_kind_combo.get_value()
  343. except ValueError:
  344. return
  345. margin = float(self.margin.get_value())
  346. gapsize = float(self.gapsize.get_value())
  347. try:
  348. gaps = self.gaps.get_value()
  349. except TypeError:
  350. self.app.inform.emit('[WARNING_NOTCL] %s' % _("Number of gaps value is missing. Add it and retry."))
  351. return
  352. if gaps not in ['None', 'LR', 'TB', '2LR', '2TB', '4', '8']:
  353. self.app.inform.emit('[WARNING_NOTCL] %s' %
  354. _("Gaps value can be only one of: 'None', 'lr', 'tb', '2lr', '2tb', 4 or 8. "
  355. "Fill in a correct value and retry. "))
  356. return
  357. if cutout_obj.multigeo is True:
  358. self.app.inform.emit('[ERROR] %s' % _("Cutout operation cannot be done on a multi-geo Geometry.\n"
  359. "Optionally, this Multi-geo Geometry can be converted to "
  360. "Single-geo Geometry,\n"
  361. "and after that perform Cutout."))
  362. return
  363. convex_box = self.convex_box.get_value()
  364. gapsize = gapsize / 2 + (dia / 2)
  365. def geo_init(geo_obj, app_obj):
  366. solid_geo = []
  367. if isinstance(cutout_obj, FlatCAMGerber):
  368. if convex_box:
  369. object_geo = cutout_obj.solid_geometry.convex_hull
  370. else:
  371. object_geo = cutout_obj.solid_geometry
  372. else:
  373. object_geo = cutout_obj.solid_geometry
  374. def cutout_handler(geom):
  375. # Get min and max data for each object as we just cut rectangles across X or Y
  376. xmin, ymin, xmax, ymax = recursive_bounds(geom)
  377. px = 0.5 * (xmin + xmax) + margin
  378. py = 0.5 * (ymin + ymax) + margin
  379. lenx = (xmax - xmin) + (margin * 2)
  380. leny = (ymax - ymin) + (margin * 2)
  381. proc_geometry = []
  382. if gaps == 'None':
  383. pass
  384. else:
  385. if gaps == '8' or gaps == '2LR':
  386. geom = self.subtract_poly_from_geo(geom,
  387. xmin - gapsize, # botleft_x
  388. py - gapsize + leny / 4, # botleft_y
  389. xmax + gapsize, # topright_x
  390. py + gapsize + leny / 4) # topright_y
  391. geom = self.subtract_poly_from_geo(geom,
  392. xmin - gapsize,
  393. py - gapsize - leny / 4,
  394. xmax + gapsize,
  395. py + gapsize - leny / 4)
  396. if gaps == '8' or gaps == '2TB':
  397. geom = self.subtract_poly_from_geo(geom,
  398. px - gapsize + lenx / 4,
  399. ymin - gapsize,
  400. px + gapsize + lenx / 4,
  401. ymax + gapsize)
  402. geom = self.subtract_poly_from_geo(geom,
  403. px - gapsize - lenx / 4,
  404. ymin - gapsize,
  405. px + gapsize - lenx / 4,
  406. ymax + gapsize)
  407. if gaps == '4' or gaps == 'LR':
  408. geom = self.subtract_poly_from_geo(geom,
  409. xmin - gapsize,
  410. py - gapsize,
  411. xmax + gapsize,
  412. py + gapsize)
  413. if gaps == '4' or gaps == 'TB':
  414. geom = self.subtract_poly_from_geo(geom,
  415. px - gapsize,
  416. ymin - gapsize,
  417. px + gapsize,
  418. ymax + gapsize)
  419. try:
  420. for g in geom:
  421. proc_geometry.append(g)
  422. except TypeError:
  423. proc_geometry.append(geom)
  424. return proc_geometry
  425. if kind == 'single':
  426. object_geo = unary_union(object_geo)
  427. # for geo in object_geo:
  428. if isinstance(cutout_obj, FlatCAMGerber):
  429. if isinstance(object_geo, MultiPolygon):
  430. x0, y0, x1, y1 = object_geo.bounds
  431. object_geo = box(x0, y0, x1, y1)
  432. geo_buf = object_geo.buffer(margin + abs(dia / 2))
  433. geo = geo_buf.exterior
  434. else:
  435. geo = object_geo
  436. solid_geo = cutout_handler(geom=geo)
  437. else:
  438. try:
  439. __ = iter(object_geo)
  440. except TypeError:
  441. object_geo = [object_geo]
  442. for geom_struct in object_geo:
  443. if isinstance(cutout_obj, FlatCAMGerber):
  444. geom_struct = (geom_struct.buffer(margin + abs(dia / 2))).exterior
  445. solid_geo += cutout_handler(geom=geom_struct)
  446. geo_obj.solid_geometry = deepcopy(solid_geo)
  447. xmin, ymin, xmax, ymax = recursive_bounds(geo_obj.solid_geometry)
  448. geo_obj.options['xmin'] = xmin
  449. geo_obj.options['ymin'] = ymin
  450. geo_obj.options['xmax'] = xmax
  451. geo_obj.options['ymax'] = ymax
  452. geo_obj.options['cnctooldia'] = str(dia)
  453. outname = cutout_obj.options["name"] + "_cutout"
  454. self.app.new_object('geometry', outname, geo_init)
  455. cutout_obj.plot()
  456. self.app.inform.emit('[success] %s' % _("Any form CutOut operation finished."))
  457. self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab)
  458. self.app.should_we_save = True
  459. def on_rectangular_cutout(self):
  460. # def subtract_rectangle(obj_, x0, y0, x1, y1):
  461. # pts = [(x0, y0), (x1, y0), (x1, y1), (x0, y1)]
  462. # obj_.subtract_polygon(pts)
  463. name = self.obj_combo.currentText()
  464. # Get source object.
  465. try:
  466. cutout_obj = self.app.collection.get_by_name(str(name))
  467. except Exception as e:
  468. log.debug("CutOut.on_rectangular_cutout() --> %s" % str(e))
  469. self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve object"), name))
  470. return "Could not retrieve object: %s" % name
  471. if cutout_obj is None:
  472. self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Object not found"), str(name)))
  473. dia = float(self.dia.get_value())
  474. if 0 in {dia}:
  475. self.app.inform.emit('[ERROR_NOTCL] %s' %
  476. _("Tool Diameter is zero value. Change it to a positive real number."))
  477. return "Tool Diameter is zero value. Change it to a positive real number."
  478. try:
  479. kind = self.obj_kind_combo.get_value()
  480. except ValueError:
  481. return
  482. margin = float(self.margin.get_value())
  483. gapsize = float(self.gapsize.get_value())
  484. try:
  485. gaps = self.gaps.get_value()
  486. except TypeError:
  487. self.app.inform.emit('[WARNING_NOTCL] %s' %
  488. _("Number of gaps value is missing. Add it and retry."))
  489. return
  490. if gaps not in ['None', 'LR', 'TB', '2LR', '2TB', '4', '8']:
  491. self.app.inform.emit('[WARNING_NOTCL] %s' % _("Gaps value can be only one of: "
  492. "'None', 'lr', 'tb', '2lr', '2tb', 4 or 8. "
  493. "Fill in a correct value and retry. "))
  494. return
  495. if cutout_obj.multigeo is True:
  496. self.app.inform.emit('[ERROR] %s' % _("Cutout operation cannot be done on a multi-geo Geometry.\n"
  497. "Optionally, this Multi-geo Geometry can be converted to "
  498. "Single-geo Geometry,\n"
  499. "and after that perform Cutout."))
  500. return
  501. # Get min and max data for each object as we just cut rectangles across X or Y
  502. gapsize = gapsize / 2 + (dia / 2)
  503. def geo_init(geo_obj, app_obj):
  504. solid_geo = []
  505. object_geo = cutout_obj.solid_geometry
  506. def cutout_rect_handler(geom):
  507. proc_geometry = []
  508. px = 0.5 * (xmin + xmax) + margin
  509. py = 0.5 * (ymin + ymax) + margin
  510. lenx = (xmax - xmin) + (margin * 2)
  511. leny = (ymax - ymin) + (margin * 2)
  512. if gaps == 'None':
  513. pass
  514. else:
  515. if gaps == '8' or gaps == '2LR':
  516. geom = self.subtract_poly_from_geo(geom,
  517. xmin - gapsize, # botleft_x
  518. py - gapsize + leny / 4, # botleft_y
  519. xmax + gapsize, # topright_x
  520. py + gapsize + leny / 4) # topright_y
  521. geom = self.subtract_poly_from_geo(geom,
  522. xmin - gapsize,
  523. py - gapsize - leny / 4,
  524. xmax + gapsize,
  525. py + gapsize - leny / 4)
  526. if gaps == '8' or gaps == '2TB':
  527. geom = self.subtract_poly_from_geo(geom,
  528. px - gapsize + lenx / 4,
  529. ymin - gapsize,
  530. px + gapsize + lenx / 4,
  531. ymax + gapsize)
  532. geom = self.subtract_poly_from_geo(geom,
  533. px - gapsize - lenx / 4,
  534. ymin - gapsize,
  535. px + gapsize - lenx / 4,
  536. ymax + gapsize)
  537. if gaps == '4' or gaps == 'LR':
  538. geom = self.subtract_poly_from_geo(geom,
  539. xmin - gapsize,
  540. py - gapsize,
  541. xmax + gapsize,
  542. py + gapsize)
  543. if gaps == '4' or gaps == 'TB':
  544. geom = self.subtract_poly_from_geo(geom,
  545. px - gapsize,
  546. ymin - gapsize,
  547. px + gapsize,
  548. ymax + gapsize)
  549. try:
  550. for g in geom:
  551. proc_geometry.append(g)
  552. except TypeError:
  553. proc_geometry.append(geom)
  554. return proc_geometry
  555. if kind == 'single':
  556. object_geo = unary_union(object_geo)
  557. xmin, ymin, xmax, ymax = object_geo.bounds
  558. geo = box(xmin, ymin, xmax, ymax)
  559. # if Gerber create a buffer at a distance
  560. # if Geometry then cut through the geometry
  561. if isinstance(cutout_obj, FlatCAMGerber):
  562. geo = geo.buffer(margin + abs(dia / 2))
  563. solid_geo = cutout_rect_handler(geom=geo)
  564. else:
  565. try:
  566. __ = iter(object_geo)
  567. except TypeError:
  568. object_geo = [object_geo]
  569. for geom_struct in object_geo:
  570. geom_struct = unary_union(geom_struct)
  571. xmin, ymin, xmax, ymax = geom_struct.bounds
  572. geom_struct = box(xmin, ymin, xmax, ymax)
  573. if isinstance(cutout_obj, FlatCAMGerber):
  574. geom_struct = geom_struct.buffer(margin + abs(dia / 2))
  575. solid_geo += cutout_rect_handler(geom=geom_struct)
  576. geo_obj.solid_geometry = deepcopy(solid_geo)
  577. geo_obj.options['cnctooldia'] = str(dia)
  578. outname = cutout_obj.options["name"] + "_cutout"
  579. self.app.new_object('geometry', outname, geo_init)
  580. # cutout_obj.plot()
  581. self.app.inform.emit('[success] %s' %
  582. _("Any form CutOut operation finished."))
  583. self.app.ui.notebook.setCurrentWidget(self.app.ui.project_tab)
  584. self.app.should_we_save = True
  585. def on_manual_gap_click(self):
  586. self.app.inform.emit(_("Click on the selected geometry object perimeter to create a bridge gap ..."))
  587. self.app.geo_editor.tool_shape.enabled = True
  588. self.cutting_dia = float(self.dia.get_value())
  589. if 0 in {self.cutting_dia}:
  590. self.app.inform.emit('[ERROR_NOTCL] %s' %
  591. _("Tool Diameter is zero value. Change it to a positive real number."))
  592. return "Tool Diameter is zero value. Change it to a positive real number."
  593. self.cutting_gapsize = float(self.gapsize.get_value())
  594. name = self.man_object_combo.currentText()
  595. # Get Geometry source object to be used as target for Manual adding Gaps
  596. try:
  597. self.man_cutout_obj = self.app.collection.get_by_name(str(name))
  598. except Exception as e:
  599. log.debug("CutOut.on_manual_cutout() --> %s" % str(e))
  600. self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve Geometry object"), name))
  601. return "Could not retrieve object: %s" % name
  602. if self.app.is_legacy is False:
  603. self.app.plotcanvas.graph_event_disconnect('key_press', self.app.ui.keyPressEvent)
  604. self.app.plotcanvas.graph_event_disconnect('mouse_press', self.app.on_mouse_click_over_plot)
  605. self.app.plotcanvas.graph_event_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot)
  606. self.app.plotcanvas.graph_event_disconnect('mouse_move', self.app.on_mouse_move_over_plot)
  607. else:
  608. self.app.plotcanvas.graph_event_disconnect(self.app.kp)
  609. self.app.plotcanvas.graph_event_disconnect(self.app.mp)
  610. self.app.plotcanvas.graph_event_disconnect(self.app.mr)
  611. self.app.plotcanvas.graph_event_disconnect(self.app.mm)
  612. self.kp = self.app.plotcanvas.graph_event_connect('key_press', self.on_key_press)
  613. self.mm = self.app.plotcanvas.graph_event_connect('mouse_move', self.on_mouse_move)
  614. self.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.on_mouse_click_release)
  615. def on_manual_cutout(self, click_pos):
  616. name = self.man_object_combo.currentText()
  617. # Get source object.
  618. try:
  619. self.man_cutout_obj = self.app.collection.get_by_name(str(name))
  620. except Exception as e:
  621. log.debug("CutOut.on_manual_cutout() --> %s" % str(e))
  622. self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve Geometry object"), name))
  623. return "Could not retrieve object: %s" % name
  624. if self.man_cutout_obj is None:
  625. self.app.inform.emit('[ERROR_NOTCL] %s: %s' %
  626. (_("Geometry object for manual cutout not found"), self.man_cutout_obj))
  627. return
  628. # use the snapped position as reference
  629. snapped_pos = self.app.geo_editor.snap(click_pos[0], click_pos[1])
  630. cut_poly = self.cutting_geo(pos=(snapped_pos[0], snapped_pos[1]))
  631. self.man_cutout_obj.subtract_polygon(cut_poly)
  632. self.man_cutout_obj.plot()
  633. self.app.inform.emit('[success] %s' % _("Added manual Bridge Gap."))
  634. self.app.should_we_save = True
  635. def on_manual_geo(self):
  636. name = self.obj_combo.currentText()
  637. # Get source object.
  638. try:
  639. cutout_obj = self.app.collection.get_by_name(str(name))
  640. except Exception as e:
  641. log.debug("CutOut.on_manual_geo() --> %s" % str(e))
  642. self.app.inform.emit('[ERROR_NOTCL] %s: %s' % (_("Could not retrieve Gerber object"), name))
  643. return "Could not retrieve object: %s" % name
  644. if cutout_obj is None:
  645. self.app.inform.emit('[ERROR_NOTCL] %s' %
  646. _("There is no Gerber object selected for Cutout.\n"
  647. "Select one and try again."))
  648. return
  649. if not isinstance(cutout_obj, FlatCAMGerber):
  650. self.app.inform.emit('[ERROR_NOTCL] %s' %
  651. _("The selected object has to be of Gerber type.\n"
  652. "Select a Gerber file and try again."))
  653. return
  654. dia = float(self.dia.get_value())
  655. if 0 in {dia}:
  656. self.app.inform.emit('[ERROR_NOTCL] %s' %
  657. _("Tool Diameter is zero value. Change it to a positive real number."))
  658. return "Tool Diameter is zero value. Change it to a positive real number."
  659. try:
  660. kind = self.obj_kind_combo.get_value()
  661. except ValueError:
  662. return
  663. margin = float(self.margin.get_value())
  664. convex_box = self.convex_box.get_value()
  665. def geo_init(geo_obj, app_obj):
  666. geo_union = unary_union(cutout_obj.solid_geometry)
  667. if convex_box:
  668. geo = geo_union.convex_hull
  669. geo_obj.solid_geometry = geo.buffer(margin + abs(dia / 2))
  670. elif kind == 'single':
  671. if isinstance(geo_union, Polygon) or \
  672. (isinstance(geo_union, list) and len(geo_union) == 1) or \
  673. (isinstance(geo_union, MultiPolygon) and len(geo_union) == 1):
  674. geo_obj.solid_geometry = geo_union.buffer(margin + abs(dia / 2)).exterior
  675. elif isinstance(geo_union, MultiPolygon):
  676. x0, y0, x1, y1 = geo_union.bounds
  677. geo = box(x0, y0, x1, y1)
  678. geo_obj.solid_geometry = geo.buffer(margin + abs(dia / 2))
  679. else:
  680. self.app.inform.emit('[ERROR_NOTCL] %s: %s' %
  681. (_("Geometry not supported for cutout"), type(geo_union)))
  682. return 'fail'
  683. else:
  684. geo = geo_union
  685. geo = geo.buffer(margin + abs(dia / 2))
  686. if isinstance(geo, Polygon):
  687. geo_obj.solid_geometry = geo.exterior
  688. elif isinstance(geo, MultiPolygon):
  689. solid_geo = []
  690. for poly in geo:
  691. solid_geo.append(poly.exterior)
  692. geo_obj.solid_geometry = deepcopy(solid_geo)
  693. geo_obj.options['cnctooldia'] = str(dia)
  694. outname = cutout_obj.options["name"] + "_cutout"
  695. self.app.new_object('geometry', outname, geo_init)
  696. def cutting_geo(self, pos):
  697. offset = self.cutting_dia / 2 + self.cutting_gapsize / 2
  698. # cutting area definition
  699. orig_x = pos[0]
  700. orig_y = pos[1]
  701. xmin = orig_x - offset
  702. ymin = orig_y - offset
  703. xmax = orig_x + offset
  704. ymax = orig_y + offset
  705. cut_poly = box(xmin, ymin, xmax, ymax)
  706. return cut_poly
  707. # To be called after clicking on the plot.
  708. def on_mouse_click_release(self, event):
  709. if self.app.is_legacy is False:
  710. event_pos = event.pos
  711. event_is_dragging = event.is_dragging
  712. right_button = 2
  713. else:
  714. event_pos = (event.xdata, event.ydata)
  715. event_is_dragging = self.app.plotcanvas.is_dragging
  716. right_button = 3
  717. try:
  718. x = float(event_pos[0])
  719. y = float(event_pos[1])
  720. except TypeError:
  721. return
  722. event_pos = (x, y)
  723. # do paint single only for left mouse clicks
  724. if event.button == 1:
  725. self.app.inform.emit(_("Making manual bridge gap..."))
  726. pos = self.app.plotcanvas.translate_coords(event_pos)
  727. self.on_manual_cutout(click_pos=pos)
  728. # if RMB then we exit
  729. elif event.button == right_button and self.mouse_is_dragging is False:
  730. if self.app.is_legacy is False:
  731. self.app.plotcanvas.graph_event_disconnect('key_press', self.on_key_press)
  732. self.app.plotcanvas.graph_event_disconnect('mouse_move', self.on_mouse_move)
  733. self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_click_release)
  734. else:
  735. self.app.plotcanvas.graph_event_disconnect(self.kp)
  736. self.app.plotcanvas.graph_event_disconnect(self.mm)
  737. self.app.plotcanvas.graph_event_disconnect(self.mr)
  738. self.app.kp = self.app.plotcanvas.graph_event_connect('key_press', self.app.ui.keyPressEvent)
  739. self.app.mp = self.app.plotcanvas.graph_event_connect('mouse_press', self.app.on_mouse_click_over_plot)
  740. self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release',
  741. self.app.on_mouse_click_release_over_plot)
  742. self.app.mm = self.app.plotcanvas.graph_event_connect('mouse_move', self.app.on_mouse_move_over_plot)
  743. # Remove any previous utility shape
  744. self.app.geo_editor.tool_shape.clear(update=True)
  745. self.app.geo_editor.tool_shape.enabled = False
  746. def on_mouse_move(self, event):
  747. self.app.on_mouse_move_over_plot(event=event)
  748. if self.app.is_legacy is False:
  749. event_pos = event.pos
  750. event_is_dragging = event.is_dragging
  751. right_button = 2
  752. else:
  753. event_pos = (event.xdata, event.ydata)
  754. event_is_dragging = self.app.plotcanvas.is_dragging
  755. right_button = 3
  756. try:
  757. x = float(event_pos[0])
  758. y = float(event_pos[1])
  759. except TypeError:
  760. return
  761. event_pos = (x, y)
  762. pos = self.canvas.translate_coords(event_pos)
  763. event.xdata, event.ydata = pos[0], pos[1]
  764. if event_is_dragging is True:
  765. self.mouse_is_dragging = True
  766. else:
  767. self.mouse_is_dragging = False
  768. try:
  769. x = float(event.xdata)
  770. y = float(event.ydata)
  771. except TypeError:
  772. return
  773. if self.app.grid_status() == True:
  774. snap_x, snap_y = self.app.geo_editor.snap(x, y)
  775. else:
  776. snap_x, snap_y = x, y
  777. self.x_pos, self.y_pos = snap_x, snap_y
  778. # #################################################
  779. # ### This section makes the cutting geo to #######
  780. # ### rotate if it intersects the target geo ######
  781. # #################################################
  782. cut_geo = self.cutting_geo(pos=(snap_x, snap_y))
  783. man_geo = self.man_cutout_obj.solid_geometry
  784. def get_angle(geo):
  785. line = cut_geo.intersection(geo)
  786. try:
  787. pt1_x = line.coords[0][0]
  788. pt1_y = line.coords[0][1]
  789. pt2_x = line.coords[1][0]
  790. pt2_y = line.coords[1][1]
  791. dx = pt1_x - pt2_x
  792. dy = pt1_y - pt2_y
  793. if dx == 0 or dy == 0:
  794. angle = 0
  795. else:
  796. radian = math.atan(dx / dy)
  797. angle = radian * 180 / math.pi
  798. except Exception as e:
  799. angle = 0
  800. return angle
  801. try:
  802. rot_angle = 0
  803. for geo_el in man_geo:
  804. if isinstance(geo_el, Polygon):
  805. work_geo = geo_el.exterior
  806. if cut_geo.intersects(work_geo):
  807. rot_angle = get_angle(geo=work_geo)
  808. else:
  809. rot_angle = 0
  810. else:
  811. rot_angle = 0
  812. if cut_geo.intersects(geo_el):
  813. rot_angle = get_angle(geo=geo_el)
  814. if rot_angle != 0:
  815. break
  816. except TypeError:
  817. if isinstance(man_geo, Polygon):
  818. work_geo = man_geo.exterior
  819. if cut_geo.intersects(work_geo):
  820. rot_angle = get_angle(geo=work_geo)
  821. else:
  822. rot_angle = 0
  823. else:
  824. rot_angle = 0
  825. if cut_geo.intersects(man_geo):
  826. rot_angle = get_angle(geo=man_geo)
  827. # rotate only if there is an angle to rotate to
  828. if rot_angle != 0:
  829. cut_geo = affinity.rotate(cut_geo, -rot_angle)
  830. # Remove any previous utility shape
  831. self.app.geo_editor.tool_shape.clear(update=True)
  832. self.draw_utility_geometry(geo=cut_geo)
  833. def draw_utility_geometry(self, geo):
  834. self.app.geo_editor.tool_shape.add(
  835. shape=geo,
  836. color=(self.app.defaults["global_draw_color"] + '80'),
  837. update=False,
  838. layer=0,
  839. tolerance=None)
  840. self.app.geo_editor.tool_shape.redraw()
  841. def on_key_press(self, event):
  842. # events out of the self.app.collection view (it's about Project Tab) are of type int
  843. if type(event) is int:
  844. key = event
  845. # events from the GUI are of type QKeyEvent
  846. elif type(event) == QtGui.QKeyEvent:
  847. key = event.key()
  848. elif isinstance(event, mpl_key_event): # MatPlotLib key events are trickier to interpret than the rest
  849. key = event.key
  850. key = QtGui.QKeySequence(key)
  851. # check for modifiers
  852. key_string = key.toString().lower()
  853. if '+' in key_string:
  854. mod, __, key_text = key_string.rpartition('+')
  855. if mod.lower() == 'ctrl':
  856. modifiers = QtCore.Qt.ControlModifier
  857. elif mod.lower() == 'alt':
  858. modifiers = QtCore.Qt.AltModifier
  859. elif mod.lower() == 'shift':
  860. modifiers = QtCore.Qt.ShiftModifier
  861. else:
  862. modifiers = QtCore.Qt.NoModifier
  863. key = QtGui.QKeySequence(key_text)
  864. # events from Vispy are of type KeyEvent
  865. else:
  866. key = event.key
  867. # Escape = Deselect All
  868. if key == QtCore.Qt.Key_Escape or key == 'Escape':
  869. if self.app.is_legacy is False:
  870. self.app.plotcanvas.graph_event_disconnect('key_press', self.on_key_press)
  871. self.app.plotcanvas.graph_event_disconnect('mouse_move', self.on_mouse_move)
  872. self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_click_release)
  873. else:
  874. self.app.plotcanvas.graph_event_disconnect(self.kp)
  875. self.app.plotcanvas.graph_event_disconnect(self.mm)
  876. self.app.plotcanvas.graph_event_disconnect(self.mr)
  877. self.app.kp = self.app.plotcanvas.graph_event_connect('key_press', self.app.ui.keyPressEvent)
  878. self.app.mp = self.app.plotcanvas.graph_event_connect('mouse_press', self.app.on_mouse_click_over_plot)
  879. self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release',
  880. self.app.on_mouse_click_release_over_plot)
  881. self.app.mm = self.app.plotcanvas.graph_event_connect('mouse_move', self.app.on_mouse_move_over_plot)
  882. # Remove any previous utility shape
  883. self.app.geo_editor.tool_shape.clear(update=True)
  884. self.app.geo_editor.tool_shape.enabled = False
  885. # Grid toggle
  886. if key == QtCore.Qt.Key_G or key == 'G':
  887. self.app.ui.grid_snap_btn.trigger()
  888. # Jump to coords
  889. if key == QtCore.Qt.Key_J or key == 'J':
  890. l_x, l_y = self.app.on_jump_to()
  891. self.app.geo_editor.tool_shape.clear(update=True)
  892. geo = self.cutting_geo(pos=(l_x, l_y))
  893. self.draw_utility_geometry(geo=geo)
  894. def subtract_poly_from_geo(self, solid_geo, x0, y0, x1, y1):
  895. """
  896. Subtract polygon made from points from the given object.
  897. This only operates on the paths in the original geometry,
  898. i.e. it converts polygons into paths.
  899. :param x0: x coord for lower left vertice of the polygon.
  900. :param y0: y coord for lower left vertice of the polygon.
  901. :param x1: x coord for upper right vertice of the polygon.
  902. :param y1: y coord for upper right vertice of the polygon.
  903. :param solid_geo: Geometry from which to substract. If none, use the solid_geomety property of the object
  904. :return: none
  905. """
  906. points = [(x0, y0), (x1, y0), (x1, y1), (x0, y1)]
  907. # pathonly should be allways True, otherwise polygons are not subtracted
  908. flat_geometry = flatten(geometry=solid_geo)
  909. log.debug("%d paths" % len(flat_geometry))
  910. polygon = Polygon(points)
  911. toolgeo = cascaded_union(polygon)
  912. diffs = []
  913. for target in flat_geometry:
  914. if type(target) == LineString or type(target) == LinearRing:
  915. diffs.append(target.difference(toolgeo))
  916. else:
  917. log.warning("Not implemented.")
  918. return unary_union(diffs)
  919. def reset_fields(self):
  920. self.obj_combo.setRootModelIndex(self.app.collection.index(0, 0, QtCore.QModelIndex()))
  921. def flatten(geometry):
  922. """
  923. Creates a list of non-iterable linear geometry objects.
  924. Polygons are expanded into its exterior and interiors.
  925. Results are placed in self.flat_geometry
  926. :param geometry: Shapely type or list or list of list of such.
  927. """
  928. flat_geo = []
  929. try:
  930. for geo in geometry:
  931. if type(geo) == Polygon:
  932. flat_geo.append(geo.exterior)
  933. for subgeo in geo.interiors:
  934. flat_geo.append(subgeo)
  935. else:
  936. flat_geo.append(geo)
  937. except TypeError:
  938. if type(geometry) == Polygon:
  939. flat_geo.append(geometry.exterior)
  940. for subgeo in geometry.interiors:
  941. flat_geo.append(subgeo)
  942. else:
  943. flat_geo.append(geometry)
  944. return flat_geo
  945. def recursive_bounds(geometry):
  946. """
  947. Returns coordinates of rectangular bounds
  948. of geometry: (xmin, ymin, xmax, ymax).
  949. """
  950. # now it can get bounds for nested lists of objects
  951. def bounds_rec(obj):
  952. try:
  953. minx = Inf
  954. miny = Inf
  955. maxx = -Inf
  956. maxy = -Inf
  957. for k in obj:
  958. minx_, miny_, maxx_, maxy_ = bounds_rec(k)
  959. minx = min(minx, minx_)
  960. miny = min(miny, miny_)
  961. maxx = max(maxx, maxx_)
  962. maxy = max(maxy, maxy_)
  963. return minx, miny, maxx, maxy
  964. except TypeError:
  965. # it's a Shapely object, return it's bounds
  966. return obj.bounds
  967. return bounds_rec(geometry)