FlatCAMDraw.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560
  1. from PyQt4 import QtGui, QtCore, Qt
  2. import FlatCAMApp
  3. from shapely.geometry import Polygon, LineString, Point, LinearRing
  4. from shapely.geometry import MultiPoint, MultiPolygon
  5. from shapely.geometry import box as shply_box
  6. from shapely.ops import cascaded_union, unary_union
  7. import shapely.affinity as affinity
  8. from shapely.wkt import loads as sloads
  9. from shapely.wkt import dumps as sdumps
  10. from shapely.geometry.base import BaseGeometry
  11. from numpy import arctan2, Inf, array, sqrt, pi, ceil, sin, cos
  12. class DrawTool(object):
  13. def __init__(self, draw_app):
  14. self.draw_app = draw_app
  15. self.complete = False
  16. self.start_msg = "Click on 1st point..."
  17. self.points = []
  18. self.geometry = None
  19. def click(self, point):
  20. return ""
  21. def utility_geometry(self, data=None):
  22. return None
  23. class FCShapeTool(DrawTool):
  24. def __init__(self, draw_app):
  25. DrawTool.__init__(self, draw_app)
  26. def make(self):
  27. pass
  28. class FCCircle(FCShapeTool):
  29. def __init__(self, draw_app):
  30. DrawTool.__init__(self, draw_app)
  31. self.start_msg = "Click on CENTER ..."
  32. def click(self, point):
  33. self.points.append(point)
  34. if len(self.points) == 1:
  35. return "Click on perimeter to complete ..."
  36. if len(self.points) == 2:
  37. self.make()
  38. return "Done."
  39. return ""
  40. def utility_geometry(self, data=None):
  41. if len(self.points) == 1:
  42. p1 = self.points[0]
  43. p2 = data
  44. radius = sqrt((p1[0]-p2[0])**2 + (p1[1]-p2[1])**2)
  45. return Point(p1).buffer(radius)
  46. return None
  47. def make(self):
  48. p1 = self.points[0]
  49. p2 = self.points[1]
  50. radius = sqrt((p1[0]-p2[0])**2 + (p1[1]-p2[1])**2)
  51. self.geometry = Point(p1).buffer(radius)
  52. self.complete = True
  53. class FCRectangle(FCShapeTool):
  54. def __init__(self, draw_app):
  55. DrawTool.__init__(self, draw_app)
  56. self.start_msg = "Click on 1st corner ..."
  57. def click(self, point):
  58. self.points.append(point)
  59. if len(self.points) == 1:
  60. return "Click on opposite corner to complete ..."
  61. if len(self.points) == 2:
  62. self.make()
  63. return "Done."
  64. return ""
  65. def utility_geometry(self, data=None):
  66. if len(self.points) == 1:
  67. p1 = self.points[0]
  68. p2 = data
  69. return LinearRing([p1, (p2[0], p1[1]), p2, (p1[0], p2[1])])
  70. return None
  71. def make(self):
  72. p1 = self.points[0]
  73. p2 = self.points[1]
  74. #self.geometry = LinearRing([p1, (p2[0], p1[1]), p2, (p1[0], p2[1])])
  75. self.geometry = Polygon([p1, (p2[0], p1[1]), p2, (p1[0], p2[1])])
  76. self.complete = True
  77. class FCPolygon(FCShapeTool):
  78. def __init__(self, draw_app):
  79. DrawTool.__init__(self, draw_app)
  80. self.start_msg = "Click on 1st point ..."
  81. def click(self, point):
  82. self.points.append(point)
  83. if len(self.points) > 0:
  84. return "Click on next point or hit SPACE to complete ..."
  85. return ""
  86. def utility_geometry(self, data=None):
  87. if len(self.points) == 1:
  88. temp_points = [x for x in self.points]
  89. temp_points.append(data)
  90. return LineString(temp_points)
  91. if len(self.points) > 1:
  92. temp_points = [x for x in self.points]
  93. temp_points.append(data)
  94. return LinearRing(temp_points)
  95. return None
  96. def make(self):
  97. # self.geometry = LinearRing(self.points)
  98. self.geometry = Polygon(self.points)
  99. self.complete = True
  100. class FCPath(FCPolygon):
  101. def make(self):
  102. self.geometry = LineString(self.points)
  103. self.complete = True
  104. def utility_geometry(self, data=None):
  105. if len(self.points) > 1:
  106. temp_points = [x for x in self.points]
  107. temp_points.append(data)
  108. return LineString(temp_points)
  109. return None
  110. class FCSelect(DrawTool):
  111. def __init__(self, draw_app):
  112. DrawTool.__init__(self, draw_app)
  113. self.shape_buffer = self.draw_app.shape_buffer
  114. self.start_msg = "Click on geometry to select"
  115. def click(self, point):
  116. min_distance = Inf
  117. closest_shape = None
  118. for shape in self.shape_buffer:
  119. if self.draw_app.key != 'control':
  120. shape["selected"] = False
  121. distance = Point(point).distance(shape["geometry"])
  122. if distance < min_distance:
  123. closest_shape = shape
  124. min_distance = distance
  125. if closest_shape is not None:
  126. closest_shape["selected"] = True
  127. return "Shape selected."
  128. return "Nothing selected."
  129. class FlatCAMDraw:
  130. def __init__(self, app, disabled=False):
  131. assert isinstance(app, FlatCAMApp.App)
  132. self.app = app
  133. self.canvas = app.plotcanvas
  134. self.axes = self.canvas.new_axes("draw")
  135. ### Drawing Toolbar ###
  136. self.drawing_toolbar = QtGui.QToolBar()
  137. self.drawing_toolbar.setDisabled(disabled)
  138. self.app.ui.addToolBar(self.drawing_toolbar)
  139. self.select_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/pointer32.png'), 'Select')
  140. self.add_circle_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/circle32.png'), 'Add Circle')
  141. self.add_rectangle_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/rectangle32.png'), 'Add Rectangle')
  142. self.add_polygon_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/polygon32.png'), 'Add Polygon')
  143. self.add_path_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/path32.png'), 'Add Path')
  144. self.union_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/union32.png'), 'Polygon Union')
  145. ### Event handlers ###
  146. ## Canvas events
  147. self.canvas.mpl_connect('button_press_event', self.on_canvas_click)
  148. self.canvas.mpl_connect('motion_notify_event', self.on_canvas_move)
  149. self.canvas.mpl_connect('key_press_event', self.on_canvas_key)
  150. self.canvas.mpl_connect('key_release_event', self.on_canvas_key_release)
  151. self.union_btn.triggered.connect(self.union)
  152. ## Toolbar events and properties
  153. self.tools = {
  154. "select": {"button": self.select_btn,
  155. "constructor": FCSelect},
  156. "circle": {"button": self.add_circle_btn,
  157. "constructor": FCCircle},
  158. "rectangle": {"button": self.add_rectangle_btn,
  159. "constructor": FCRectangle},
  160. "polygon": {"button": self.add_polygon_btn,
  161. "constructor": FCPolygon},
  162. "path": {"button": self.add_path_btn,
  163. "constructor": FCPath}
  164. }
  165. # Data
  166. self.active_tool = None
  167. self.shape_buffer = []
  168. self.move_timer = QtCore.QTimer()
  169. self.move_timer.setSingleShot(True)
  170. self.key = None # Currently pressed key
  171. def make_callback(tool):
  172. def f():
  173. self.on_tool_select(tool)
  174. return f
  175. for tool in self.tools:
  176. self.tools[tool]["button"].triggered.connect(make_callback(tool)) # Events
  177. self.tools[tool]["button"].setCheckable(True) # Checkable
  178. def clear(self):
  179. self.active_tool = None
  180. self.shape_buffer = []
  181. self.replot()
  182. def on_tool_select(self, tool):
  183. """
  184. :rtype : None
  185. """
  186. self.app.log.debug("on_tool_select('%s')" % tool)
  187. # This is to make the group behave as radio group
  188. if tool in self.tools:
  189. if self.tools[tool]["button"].isChecked():
  190. self.app.log.debug("%s is checked.")
  191. for t in self.tools:
  192. if t != tool:
  193. self.tools[t]["button"].setChecked(False)
  194. self.active_tool = self.tools[tool]["constructor"](self)
  195. self.app.info(self.active_tool.start_msg)
  196. else:
  197. self.app.log.debug("%s is NOT checked.")
  198. for t in self.tools:
  199. self.tools[t]["button"].setChecked(False)
  200. self.active_tool = None
  201. def on_canvas_click(self, event):
  202. """
  203. event.x .y have canvas coordinates
  204. event.xdaya .ydata have plot coordinates
  205. :param event:
  206. :return:
  207. """
  208. if self.active_tool is not None:
  209. # Dispatch event to active_tool
  210. msg = self.active_tool.click((event.xdata, event.ydata))
  211. self.app.info(msg)
  212. # If it is a shape generating tool
  213. if isinstance(self.active_tool, FCShapeTool) and self.active_tool.complete:
  214. self.on_shape_complete()
  215. return
  216. if isinstance(self.active_tool, FCSelect):
  217. self.app.log.debug("Replotting after click.")
  218. self.replot()
  219. def on_canvas_move(self, event):
  220. """
  221. event.x .y have canvas coordinates
  222. event.xdaya .ydata have plot coordinates
  223. :param event:
  224. :return:
  225. """
  226. self.on_canvas_move_effective(event)
  227. return
  228. self.move_timer.stop()
  229. if self.active_tool is None:
  230. return
  231. # Make a function to avoid late evaluation
  232. def make_callback():
  233. def f():
  234. self.on_canvas_move_effective(event)
  235. return f
  236. callback = make_callback()
  237. self.move_timer.timeout.connect(callback)
  238. self.move_timer.start(500) # Stops if aready running
  239. def on_canvas_move_effective(self, event):
  240. """
  241. Is called after timeout on timer set in on_canvas_move.
  242. For details on animating on MPL see:
  243. http://wiki.scipy.org/Cookbook/Matplotlib/Animations
  244. event.x .y have canvas coordinates
  245. event.xdaya .ydata have plot coordinates
  246. :param event:
  247. :return:
  248. """
  249. try:
  250. x = float(event.xdata)
  251. y = float(event.ydata)
  252. except TypeError:
  253. return
  254. if self.active_tool is None:
  255. return
  256. geo = self.active_tool.utility_geometry(data=(x, y))
  257. if geo is not None:
  258. # Remove any previous utility shape
  259. for shape in self.shape_buffer:
  260. if shape['utility']:
  261. self.shape_buffer.remove(shape)
  262. # Add the new utility shape
  263. self.shape_buffer.append({
  264. 'geometry': geo,
  265. 'selected': False,
  266. 'utility': True
  267. })
  268. # Efficient plotting for fast animation
  269. elements = self.plot_shape(geometry=geo, linespec="b--", animated=True)
  270. self.canvas.canvas.restore_region(self.canvas.background)
  271. for el in elements:
  272. self.axes.draw_artist(el)
  273. self.canvas.canvas.blit(self.axes.bbox)
  274. #self.replot()
  275. def on_canvas_key(self, event):
  276. """
  277. event.key has the key.
  278. :param event:
  279. :return:
  280. """
  281. self.key = event.key
  282. ### Finish the current action. Use with tools that do not
  283. ### complete automatically, like a polygon or path.
  284. if event.key == ' ':
  285. if isinstance(self.active_tool, FCShapeTool):
  286. self.active_tool.click((event.xdata, event.ydata))
  287. self.active_tool.make()
  288. if self.active_tool.complete:
  289. self.on_shape_complete()
  290. return
  291. ### Abort the current action
  292. if event.key == 'escape':
  293. # TODO: ...?
  294. self.on_tool_select("select")
  295. self.app.info("Cancelled.")
  296. for_deletion = [shape for shape in self.shape_buffer if shape['utility']]
  297. for shape in for_deletion:
  298. self.shape_buffer.remove(shape)
  299. self.replot()
  300. return
  301. ### Delete selected object
  302. if event.key == '-':
  303. self.delete_selected()
  304. self.replot()
  305. def on_canvas_key_release(self, event):
  306. self.key = None
  307. def delete_selected(self):
  308. for_deletion = [shape for shape in self.shape_buffer if shape["selected"]]
  309. for shape in for_deletion:
  310. self.shape_buffer.remove(shape)
  311. self.app.info("Shape deleted.")
  312. def plot_shape(self, geometry=None, linespec='b-', linewidth=1, animated=False):
  313. self.app.log.debug("plot_shape()")
  314. plot_elements = []
  315. if geometry is None:
  316. geometry = self.active_tool.geometry
  317. try:
  318. _ = iter(geometry)
  319. iterable_geometry = geometry
  320. except TypeError:
  321. iterable_geometry = [geometry]
  322. for geo in iterable_geometry:
  323. if type(geo) == Polygon:
  324. x, y = geo.exterior.coords.xy
  325. element, = self.axes.plot(x, y, linespec, linewidth=linewidth, animated=animated)
  326. plot_elements.append(element)
  327. for ints in geo.interiors:
  328. x, y = ints.coords.xy
  329. element, = self.axes.plot(x, y, linespec, linewidth=linewidth, animated=animated)
  330. plot_elements.append(element)
  331. continue
  332. if type(geo) == LineString or type(geo) == LinearRing:
  333. x, y = geo.coords.xy
  334. element, = self.axes.plot(x, y, linespec, linewidth=linewidth, animated=animated)
  335. plot_elements.append(element)
  336. continue
  337. if type(geo) == MultiPolygon:
  338. for poly in geo:
  339. x, y = poly.exterior.coords.xy
  340. element, = self.axes.plot(x, y, linespec, linewidth=linewidth, animated=animated)
  341. plot_elements.append(element)
  342. for ints in poly.interiors:
  343. x, y = ints.coords.xy
  344. element, = self.axes.plot(x, y, linespec, linewidth=linewidth, animated=animated)
  345. plot_elements.append(element)
  346. continue
  347. return plot_elements
  348. # self.canvas.auto_adjust_axes()
  349. def plot_all(self):
  350. self.app.log.debug("plot_all()")
  351. self.axes.cla()
  352. for shape in self.shape_buffer:
  353. if shape['utility']:
  354. self.plot_shape(geometry=shape['geometry'], linespec='k--', linewidth=1)
  355. continue
  356. if shape['selected']:
  357. self.plot_shape(geometry=shape['geometry'], linespec='k-', linewidth=2)
  358. continue
  359. self.plot_shape(geometry=shape['geometry'])
  360. self.canvas.auto_adjust_axes()
  361. def on_shape_complete(self):
  362. self.app.log.debug("on_shape_complete()")
  363. # For some reason plotting just the last created figure does not
  364. # work. The figure is not shown. Calling replot does the trick
  365. # which generates a new axes object.
  366. #self.plot_shape()
  367. #self.canvas.auto_adjust_axes()
  368. self.shape_buffer.append({'geometry': self.active_tool.geometry,
  369. 'selected': False,
  370. 'utility': False})
  371. # Remove any utility shapes
  372. for shape in self.shape_buffer:
  373. if shape['utility']:
  374. self.shape_buffer.remove(shape)
  375. self.replot()
  376. self.active_tool = type(self.active_tool)(self)
  377. def replot(self):
  378. #self.canvas.clear()
  379. self.axes = self.canvas.new_axes("draw")
  380. self.plot_all()
  381. def edit_fcgeometry(self, fcgeometry):
  382. """
  383. Imports the geometry from the given FlatCAM Geometry object
  384. into the editor.
  385. :param fcgeometry: FlatCAMGeometry
  386. :return: None
  387. """
  388. try:
  389. _ = iter(fcgeometry.solid_geometry)
  390. geometry = fcgeometry.solid_geometry
  391. except TypeError:
  392. geometry = [fcgeometry.solid_geometry]
  393. # Delete contents of editor.
  394. self.shape_buffer = []
  395. # Link shapes into editor.
  396. for shape in geometry:
  397. self.shape_buffer.append({'geometry': shape,
  398. 'selected': False,
  399. 'utility': False})
  400. self.replot()
  401. self.drawing_toolbar.setDisabled(False)
  402. def update_fcgeometry(self, fcgeometry):
  403. """
  404. Transfers the drawing tool shape buffer to the selected geometry
  405. object. The geometry already in the object are removed.
  406. :param fcgeometry: FlatCAMGeometry
  407. :return: None
  408. """
  409. fcgeometry.solid_geometry = []
  410. for shape in self.shape_buffer:
  411. fcgeometry.solid_geometry.append(shape['geometry'])
  412. def union(self):
  413. """
  414. Makes union of selected polygons. Original polygons
  415. are deleted.
  416. :return: None.
  417. """
  418. targets = [shape for shape in self.shape_buffer if shape['selected']]
  419. results = cascaded_union([t['geometry'] for t in targets])
  420. for shape in targets:
  421. self.shape_buffer.remove(shape)
  422. try:
  423. for geo in results:
  424. self.shape_buffer.append({
  425. 'geometry': geo,
  426. 'selected': True,
  427. 'utility': False
  428. })
  429. except TypeError:
  430. self.shape_buffer.append({
  431. 'geometry': results,
  432. 'selected': True,
  433. 'utility': False
  434. })
  435. self.replot()