FlatCAMCommon.py 20 KB

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