FlatCAMCommon.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570
  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 (major mod): Marius Adrian Stanciu #
  10. # Date: 11/4/2019 #
  11. # ##########################################################
  12. from PyQt5 import QtCore
  13. from shapely.geometry import Polygon, MultiPolygon
  14. from flatcamGUI.VisPyVisuals import ShapeCollection
  15. from FlatCAMTool import FlatCAMTool
  16. import numpy as np
  17. import gettext
  18. import FlatCAMTranslation as fcTranslate
  19. import builtins
  20. fcTranslate.apply_language('strings')
  21. if '_' not in builtins.__dict__:
  22. _ = gettext.gettext
  23. class GracefulException(Exception):
  24. # Graceful Exception raised when the user is requesting to cancel the current threaded task
  25. def __init__(self):
  26. super().__init__()
  27. def __str__(self):
  28. return '\n\n%s' % _("The user requested a graceful exit of the current task.")
  29. class LoudDict(dict):
  30. """
  31. A Dictionary with a callback for item changes.
  32. """
  33. def __init__(self, *args, **kwargs):
  34. dict.__init__(self, *args, **kwargs)
  35. self.callback = lambda x: None
  36. def __setitem__(self, key, value):
  37. """
  38. Overridden __setitem__ method. Will emit 'changed(QString)' if the item was changed, with key as parameter.
  39. """
  40. if key in self and self.__getitem__(key) == value:
  41. return
  42. dict.__setitem__(self, key, value)
  43. self.callback(key)
  44. def update(self, *args, **kwargs):
  45. if len(args) > 1:
  46. raise TypeError("update expected at most 1 arguments, got %d" % len(args))
  47. other = dict(*args, **kwargs)
  48. for key in other:
  49. self[key] = other[key]
  50. def set_change_callback(self, callback):
  51. """
  52. Assigns a function as callback on item change. The callback
  53. will receive the key of the object that was changed.
  54. :param callback: Function to call on item change.
  55. :type callback: func
  56. :return: None
  57. """
  58. self.callback = callback
  59. class FCSignal:
  60. """
  61. Taken from here: https://blog.abstractfactory.io/dynamic-signals-in-pyqt/
  62. """
  63. def __init__(self):
  64. self.__subscribers = []
  65. def emit(self, *args, **kwargs):
  66. for subs in self.__subscribers:
  67. subs(*args, **kwargs)
  68. def connect(self, func):
  69. self.__subscribers.append(func)
  70. def disconnect(self, func):
  71. try:
  72. self.__subscribers.remove(func)
  73. except ValueError:
  74. print('Warning: function %s not removed '
  75. 'from signal %s' % (func, self))
  76. def color_variant(hex_color, bright_factor=1):
  77. """
  78. Takes a color in HEX format #FF00FF and produces a lighter or darker variant
  79. :param hex_color: color to change
  80. :param bright_factor: factor to change the color brightness [0 ... 1]
  81. :return: modified color
  82. """
  83. if len(hex_color) != 7:
  84. print("Color is %s, but needs to be in #FF00FF format. Returning original color." % hex_color)
  85. return hex_color
  86. if bright_factor > 1.0:
  87. bright_factor = 1.0
  88. if bright_factor < 0.0:
  89. bright_factor = 0.0
  90. rgb_hex = [hex_color[x:x + 2] for x in [1, 3, 5]]
  91. new_rgb = []
  92. for hex_value in rgb_hex:
  93. # adjust each color channel and turn it into a INT suitable as argument for hex()
  94. mod_color = round(int(hex_value, 16) * bright_factor)
  95. # make sure that each color channel has two digits without the 0x prefix
  96. mod_color_hex = str(hex(mod_color)[2:]).zfill(2)
  97. new_rgb.append(mod_color_hex)
  98. return "#" + "".join([i for i in new_rgb])
  99. class ExclusionAreas(QtCore.QObject):
  100. e_shape_modified = QtCore.pyqtSignal()
  101. def __init__(self, app):
  102. super().__init__()
  103. self.app = app
  104. # Storage for shapes, storage that can be used by FlatCAm tools for utility geometry
  105. # VisPy visuals
  106. if self.app.is_legacy is False:
  107. try:
  108. self.exclusion_shapes = ShapeCollection(parent=self.app.plotcanvas.view.scene, layers=1)
  109. except AttributeError:
  110. self.exclusion_shapes = None
  111. else:
  112. from flatcamGUI.PlotCanvasLegacy import ShapeCollectionLegacy
  113. self.exclusion_shapes = ShapeCollectionLegacy(obj=self, app=self.app, name="exclusion")
  114. # Event signals disconnect id holders
  115. self.mr = None
  116. self.mm = None
  117. self.kp = None
  118. # variables to be used in area exclusion
  119. self.cursor_pos = (0, 0)
  120. self.first_click = False
  121. self.points = []
  122. self.poly_drawn = False
  123. '''
  124. Here we store the exclusion shapes and some other information's
  125. Each list element is a dictionary with the format:
  126. {
  127. "obj_type": string ("excellon" or "geometry") <- self.obj_type
  128. "shape": Shapely polygon
  129. "strategy": string ("over" or "around") <- self.strategy
  130. "overz": float <- self.over_z
  131. }
  132. '''
  133. self.exclusion_areas_storage = []
  134. self.mouse_is_dragging = False
  135. self.solid_geometry = []
  136. self.obj_type = None
  137. self.shape_type = 'square' # TODO use the self.app.defaults when made general (not in Geo object Pref UI)
  138. self.over_z = 0.1
  139. self.strategy = None
  140. self.cnc_button = None
  141. def on_add_area_click(self, shape_button, overz_button, strategy_radio, cnc_button, solid_geo, obj_type):
  142. """
  143. :param shape_button: a FCButton that has the value for the shape
  144. :param overz_button: a FCDoubleSpinner that holds the Over Z value
  145. :param strategy_radio: a RadioSet button with the strategy value
  146. :param cnc_button: a FCButton in Object UI that when clicked the CNCJob is created
  147. We have a reference here so we can change the color signifying that exclusion areas are
  148. available.
  149. :param solid_geo: reference to the object solid geometry for which we add exclusion areas
  150. :param obj_type: Type of FlatCAM object that called this method
  151. :type obj_type: String: "excellon" or "geometry"
  152. :return:
  153. """
  154. self.app.inform.emit('[WARNING_NOTCL] %s' % _("Click the start point of the area."))
  155. self.app.call_source = 'geometry'
  156. self.shape_type = shape_button.get_value()
  157. self.over_z = overz_button.get_value()
  158. self.strategy = strategy_radio.get_value()
  159. self.cnc_button = cnc_button
  160. self.solid_geometry = solid_geo
  161. self.obj_type = obj_type
  162. if self.app.is_legacy is False:
  163. self.app.plotcanvas.graph_event_disconnect('mouse_press', self.app.on_mouse_click_over_plot)
  164. self.app.plotcanvas.graph_event_disconnect('mouse_move', self.app.on_mouse_move_over_plot)
  165. self.app.plotcanvas.graph_event_disconnect('mouse_release', self.app.on_mouse_click_release_over_plot)
  166. else:
  167. self.app.plotcanvas.graph_event_disconnect(self.app.mp)
  168. self.app.plotcanvas.graph_event_disconnect(self.app.mm)
  169. self.app.plotcanvas.graph_event_disconnect(self.app.mr)
  170. self.mr = self.app.plotcanvas.graph_event_connect('mouse_release', self.on_mouse_release)
  171. self.mm = self.app.plotcanvas.graph_event_connect('mouse_move', self.on_mouse_move)
  172. # self.kp = self.app.plotcanvas.graph_event_connect('key_press', self.on_key_press)
  173. # To be called after clicking on the plot.
  174. def on_mouse_release(self, event):
  175. if self.app.is_legacy is False:
  176. event_pos = event.pos
  177. # event_is_dragging = event.is_dragging
  178. right_button = 2
  179. else:
  180. event_pos = (event.xdata, event.ydata)
  181. # event_is_dragging = self.app.plotcanvas.is_dragging
  182. right_button = 3
  183. event_pos = self.app.plotcanvas.translate_coords(event_pos)
  184. if self.app.grid_status():
  185. curr_pos = self.app.geo_editor.snap(event_pos[0], event_pos[1])
  186. else:
  187. curr_pos = (event_pos[0], event_pos[1])
  188. x1, y1 = curr_pos[0], curr_pos[1]
  189. # shape_type = self.ui.area_shape_radio.get_value()
  190. # do clear area only for left mouse clicks
  191. if event.button == 1:
  192. if self.shape_type == "square":
  193. if self.first_click is False:
  194. self.first_click = True
  195. self.app.inform.emit('[WARNING_NOTCL] %s' % _("Click the end point of the area."))
  196. self.cursor_pos = self.app.plotcanvas.translate_coords(event_pos)
  197. if self.app.grid_status():
  198. self.cursor_pos = self.app.geo_editor.snap(event_pos[0], event_pos[1])
  199. else:
  200. self.app.inform.emit(_("Zone added. Click to start adding next zone or right click to finish."))
  201. self.app.delete_selection_shape()
  202. x0, y0 = self.cursor_pos[0], self.cursor_pos[1]
  203. pt1 = (x0, y0)
  204. pt2 = (x1, y0)
  205. pt3 = (x1, y1)
  206. pt4 = (x0, y1)
  207. new_rectangle = Polygon([pt1, pt2, pt3, pt4])
  208. # {
  209. # "obj_type": string("excellon" or "geometry") < - self.obj_type
  210. # "shape": Shapely polygon
  211. # "strategy": string("over" or "around") < - self.strategy
  212. # "overz": float < - self.over_z
  213. # }
  214. new_el = {
  215. "obj_type": self.obj_type,
  216. "shape": new_rectangle,
  217. "strategy": self.strategy,
  218. "overz": self.over_z
  219. }
  220. self.exclusion_areas_storage.append(new_el)
  221. if self.obj_type == 'excellon':
  222. color = "#FF7400"
  223. face_color = "#FF7400BF"
  224. else:
  225. color = "#098a8f"
  226. face_color = "#FF7400BF"
  227. # add a temporary shape on canvas
  228. FlatCAMTool.draw_tool_selection_shape(
  229. self, old_coords=(x0, y0), coords=(x1, y1),
  230. color=color,
  231. face_color=face_color,
  232. shapes_storage=self.exclusion_shapes)
  233. self.first_click = False
  234. return
  235. else:
  236. self.points.append((x1, y1))
  237. if len(self.points) > 1:
  238. self.poly_drawn = True
  239. self.app.inform.emit(_("Click on next Point or click right mouse button to complete ..."))
  240. return ""
  241. elif event.button == right_button and self.mouse_is_dragging is False:
  242. shape_type = self.shape_type
  243. if shape_type == "square":
  244. self.first_click = False
  245. else:
  246. # if we finish to add a polygon
  247. if self.poly_drawn is True:
  248. try:
  249. # try to add the point where we last clicked if it is not already in the self.points
  250. last_pt = (x1, y1)
  251. if last_pt != self.points[-1]:
  252. self.points.append(last_pt)
  253. except IndexError:
  254. pass
  255. # we need to add a Polygon and a Polygon can be made only from at least 3 points
  256. if len(self.points) > 2:
  257. FlatCAMTool.delete_moving_selection_shape(self)
  258. pol = Polygon(self.points)
  259. # do not add invalid polygons even if they are drawn by utility geometry
  260. if pol.is_valid:
  261. # {
  262. # "obj_type": string("excellon" or "geometry") < - self.obj_type
  263. # "shape": Shapely polygon
  264. # "strategy": string("over" or "around") < - self.strategy
  265. # "overz": float < - self.over_z
  266. # }
  267. new_el = {
  268. "obj_type": self.obj_type,
  269. "shape": pol,
  270. "strategy": self.strategy,
  271. "overz": self.over_z
  272. }
  273. self.exclusion_areas_storage.append(new_el)
  274. if self.obj_type == 'excellon':
  275. color = "#FF7400"
  276. face_color = "#FF7400BF"
  277. else:
  278. color = "#098a8f"
  279. face_color = "#FF7400BF"
  280. FlatCAMTool.draw_selection_shape_polygon(
  281. self, points=self.points,
  282. color=color,
  283. face_color=face_color,
  284. shapes_storage=self.exclusion_shapes)
  285. self.app.inform.emit(
  286. _("Zone added. Click to start adding next zone or right click to finish."))
  287. self.points = []
  288. self.poly_drawn = False
  289. return
  290. # FlatCAMTool.delete_tool_selection_shape(self, shapes_storage=self.exclusion_shapes)
  291. if self.app.is_legacy is False:
  292. self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_release)
  293. self.app.plotcanvas.graph_event_disconnect('mouse_move', self.on_mouse_move)
  294. # self.app.plotcanvas.graph_event_disconnect('key_press', self.on_key_press)
  295. else:
  296. self.app.plotcanvas.graph_event_disconnect(self.mr)
  297. self.app.plotcanvas.graph_event_disconnect(self.mm)
  298. # self.app.plotcanvas.graph_event_disconnect(self.kp)
  299. self.app.mp = self.app.plotcanvas.graph_event_connect('mouse_press',
  300. self.app.on_mouse_click_over_plot)
  301. self.app.mm = self.app.plotcanvas.graph_event_connect('mouse_move',
  302. self.app.on_mouse_move_over_plot)
  303. self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release',
  304. self.app.on_mouse_click_release_over_plot)
  305. self.app.call_source = 'app'
  306. if len(self.exclusion_areas_storage) == 0:
  307. return
  308. self.app.inform.emit(
  309. "[success] %s" % _("Exclusion areas added. Checking overlap with the object geometry ..."))
  310. for el in self.exclusion_areas_storage:
  311. if el["shape"].intersects(MultiPolygon(self.solid_geometry)):
  312. self.on_clear_area_click()
  313. self.app.inform.emit(
  314. "[ERROR_NOTCL] %s" % _("Failed. Exclusion areas intersects the object geometry ..."))
  315. return
  316. self.app.inform.emit(
  317. "[success] %s" % _("Exclusion areas added."))
  318. self.cnc_button.setStyleSheet("""
  319. QPushButton
  320. {
  321. font-weight: bold;
  322. color: orange;
  323. }
  324. """)
  325. self.cnc_button.setToolTip(
  326. '%s %s' % (_("Generate the CNC Job object."), _("With Exclusion areas."))
  327. )
  328. self.e_shape_modified.emit()
  329. for k in self.exclusion_areas_storage:
  330. print(k)
  331. def area_disconnect(self):
  332. if self.app.is_legacy is False:
  333. self.app.plotcanvas.graph_event_disconnect('mouse_release', self.on_mouse_release)
  334. self.app.plotcanvas.graph_event_disconnect('mouse_move', self.on_mouse_move)
  335. else:
  336. self.app.plotcanvas.graph_event_disconnect(self.mr)
  337. self.app.plotcanvas.graph_event_disconnect(self.mm)
  338. self.app.plotcanvas.graph_event_disconnect(self.kp)
  339. self.app.mp = self.app.plotcanvas.graph_event_connect('mouse_press',
  340. self.app.on_mouse_click_over_plot)
  341. self.app.mm = self.app.plotcanvas.graph_event_connect('mouse_move',
  342. self.app.on_mouse_move_over_plot)
  343. self.app.mr = self.app.plotcanvas.graph_event_connect('mouse_release',
  344. self.app.on_mouse_click_release_over_plot)
  345. self.points = []
  346. self.poly_drawn = False
  347. self.exclusion_areas_storage = []
  348. FlatCAMTool.delete_moving_selection_shape(self)
  349. # FlatCAMTool.delete_tool_selection_shape(self, shapes_storage=self.exclusion_shapes)
  350. self.app.call_source = "app"
  351. self.app.inform.emit("[WARNING_NOTCL] %s" % _("Cancelled. Area exclusion drawing was interrupted."))
  352. # called on mouse move
  353. def on_mouse_move(self, event):
  354. shape_type = self.shape_type
  355. if self.app.is_legacy is False:
  356. event_pos = event.pos
  357. event_is_dragging = event.is_dragging
  358. # right_button = 2
  359. else:
  360. event_pos = (event.xdata, event.ydata)
  361. event_is_dragging = self.app.plotcanvas.is_dragging
  362. # right_button = 3
  363. curr_pos = self.app.plotcanvas.translate_coords(event_pos)
  364. # detect mouse dragging motion
  365. if event_is_dragging is True:
  366. self.mouse_is_dragging = True
  367. else:
  368. self.mouse_is_dragging = False
  369. # update the cursor position
  370. if self.app.grid_status():
  371. # Update cursor
  372. curr_pos = self.app.geo_editor.snap(curr_pos[0], curr_pos[1])
  373. self.app.app_cursor.set_data(np.asarray([(curr_pos[0], curr_pos[1])]),
  374. symbol='++', edge_color=self.app.cursor_color_3D,
  375. edge_width=self.app.defaults["global_cursor_width"],
  376. size=self.app.defaults["global_cursor_size"])
  377. # update the positions on status bar
  378. self.app.ui.position_label.setText("&nbsp;&nbsp;&nbsp;&nbsp;<b>X</b>: %.4f&nbsp;&nbsp; "
  379. "<b>Y</b>: %.4f" % (curr_pos[0], curr_pos[1]))
  380. if self.cursor_pos is None:
  381. self.cursor_pos = (0, 0)
  382. self.app.dx = curr_pos[0] - float(self.cursor_pos[0])
  383. self.app.dy = curr_pos[1] - float(self.cursor_pos[1])
  384. self.app.ui.rel_position_label.setText("<b>Dx</b>: %.4f&nbsp;&nbsp; <b>Dy</b>: "
  385. "%.4f&nbsp;&nbsp;&nbsp;&nbsp;" % (self.app.dx, self.app.dy))
  386. if self.obj_type == 'excellon':
  387. color = "#FF7400"
  388. face_color = "#FF7400BF"
  389. else:
  390. color = "#098a8f"
  391. face_color = "#FF7400BF"
  392. # draw the utility geometry
  393. if shape_type == "square":
  394. if self.first_click:
  395. self.app.delete_selection_shape()
  396. self.app.draw_moving_selection_shape(old_coords=(self.cursor_pos[0], self.cursor_pos[1]),
  397. color=color,
  398. face_color=face_color,
  399. coords=(curr_pos[0], curr_pos[1]))
  400. else:
  401. FlatCAMTool.delete_moving_selection_shape(self)
  402. FlatCAMTool.draw_moving_selection_shape_poly(
  403. self, points=self.points,
  404. color=color,
  405. face_color=face_color,
  406. data=(curr_pos[0], curr_pos[1]))
  407. def on_clear_area_click(self):
  408. self.clear_shapes()
  409. # restore the default StyleSheet
  410. self.cnc_button.setStyleSheet("")
  411. # update the StyleSheet
  412. self.cnc_button.setStyleSheet("""
  413. QPushButton
  414. {
  415. font-weight: bold;
  416. }
  417. """)
  418. self.cnc_button.setToolTip('%s' % _("Generate the CNC Job object."))
  419. def clear_shapes(self):
  420. self.exclusion_areas_storage.clear()
  421. FlatCAMTool.delete_moving_selection_shape(self)
  422. self.app.delete_selection_shape()
  423. FlatCAMTool.delete_tool_selection_shape(self, shapes_storage=self.exclusion_shapes)
  424. self.app.inform.emit('[success] %s' % _("All exclusion zones deleted."))
  425. def delete_sel_shapes(self, idxs):
  426. """
  427. :param idxs: list of indexes in self.exclusion_areas_storage list to be deleted
  428. :return:
  429. """
  430. # delete all plotted shapes
  431. FlatCAMTool.delete_tool_selection_shape(self, shapes_storage=self.exclusion_shapes)
  432. # delete shapes
  433. for idx in sorted(idxs, reverse=True):
  434. del self.exclusion_areas_storage[idx]
  435. # re-add what's left after deletion in first step
  436. if self.obj_type == 'excellon':
  437. color = "#FF7400"
  438. face_color = "#FF7400BF"
  439. else:
  440. color = "#098a8f"
  441. face_color = "#FF7400BF"
  442. face_alpha = 0.3
  443. color_t = face_color[:-2] + str(hex(int(face_alpha * 255)))[2:]
  444. for geo_el in self.exclusion_areas_storage:
  445. if isinstance(geo_el['shape'], Polygon):
  446. self.exclusion_shapes.add(
  447. geo_el['shape'], color=color, face_color=color_t, update=True, layer=0, tolerance=None)
  448. if self.app.is_legacy is True:
  449. self.exclusion_shapes.redraw()
  450. if self.exclusion_areas_storage:
  451. self.app.inform.emit('[success] %s' % _("Selected exclusion zones deleted."))
  452. else:
  453. # restore the default StyleSheet
  454. self.cnc_button.setStyleSheet("")
  455. # update the StyleSheet
  456. self.cnc_button.setStyleSheet("""
  457. QPushButton
  458. {
  459. font-weight: bold;
  460. }
  461. """)
  462. self.cnc_button.setToolTip('%s' % _("Generate the CNC Job object."))
  463. self.app.inform.emit('[success] %s' % _("All exclusion zones deleted."))