FlatCAMDraw.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639
  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 FCMove(FCShapeTool):
  130. def __init__(self, draw_app):
  131. FCShapeTool.__init__(self, draw_app)
  132. self.shape_buffer = self.draw_app.shape_buffer
  133. self.origin = None
  134. self.destination = None
  135. self.start_msg = "Click on reference point."
  136. def set_origin(self, origin):
  137. self.origin = origin
  138. def click(self, point):
  139. if self.origin is None:
  140. self.set_origin(point)
  141. return "Click on final location."
  142. else:
  143. self.destination = point
  144. self.make()
  145. return "Done."
  146. def make(self):
  147. # Create new geometry
  148. dx = self.destination[0] - self.origin[0]
  149. dy = self.destination[1] - self.origin[1]
  150. self.geometry = [affinity.translate(geom['geometry'], xoff=dx, yoff=dy) for geom in self.draw_app.get_selected()]
  151. # Delete old
  152. for geo in self.draw_app.get_selected():
  153. self.draw_app.shape_buffer.remove(geo)
  154. self.complete = True
  155. def utility_geometry(self, data=None):
  156. if self.origin is None:
  157. return None
  158. dx = data[0] - self.origin[0]
  159. dy = data[1] - self.origin[1]
  160. return [affinity.translate(geom['geometry'], xoff=dx, yoff=dy) for geom in self.draw_app.get_selected()]
  161. class FCCopy(FCMove):
  162. def make(self):
  163. # Create new geometry
  164. dx = self.destination[0] - self.origin[0]
  165. dy = self.destination[1] - self.origin[1]
  166. self.geometry = [affinity.translate(geom['geometry'], xoff=dx, yoff=dy) for geom in self.draw_app.get_selected()]
  167. self.complete = True
  168. class FlatCAMDraw:
  169. def __init__(self, app, disabled=False):
  170. assert isinstance(app, FlatCAMApp.App)
  171. self.app = app
  172. self.canvas = app.plotcanvas
  173. self.axes = self.canvas.new_axes("draw")
  174. ### Drawing Toolbar ###
  175. self.drawing_toolbar = QtGui.QToolBar()
  176. self.drawing_toolbar.setDisabled(disabled)
  177. self.app.ui.addToolBar(self.drawing_toolbar)
  178. self.select_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/pointer32.png'), 'Select')
  179. self.add_circle_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/circle32.png'), 'Add Circle')
  180. self.add_rectangle_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/rectangle32.png'), 'Add Rectangle')
  181. self.add_polygon_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/polygon32.png'), 'Add Polygon')
  182. self.add_path_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/path32.png'), 'Add Path')
  183. self.union_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/union32.png'), 'Polygon Union')
  184. self.move_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/move32.png'), 'Move Objects')
  185. self.copy_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/copy32.png'), 'Copy Objects')
  186. ### Event handlers ###
  187. ## Canvas events
  188. self.canvas.mpl_connect('button_press_event', self.on_canvas_click)
  189. self.canvas.mpl_connect('motion_notify_event', self.on_canvas_move)
  190. self.canvas.mpl_connect('key_press_event', self.on_canvas_key)
  191. self.canvas.mpl_connect('key_release_event', self.on_canvas_key_release)
  192. self.union_btn.triggered.connect(self.union)
  193. ## Toolbar events and properties
  194. self.tools = {
  195. "select": {"button": self.select_btn,
  196. "constructor": FCSelect},
  197. "circle": {"button": self.add_circle_btn,
  198. "constructor": FCCircle},
  199. "rectangle": {"button": self.add_rectangle_btn,
  200. "constructor": FCRectangle},
  201. "polygon": {"button": self.add_polygon_btn,
  202. "constructor": FCPolygon},
  203. "path": {"button": self.add_path_btn,
  204. "constructor": FCPath},
  205. "move": {"button": self.move_btn,
  206. "constructor": FCMove},
  207. "copy": {"button": self.copy_btn,
  208. "constructor": FCCopy}
  209. }
  210. # Data
  211. self.active_tool = None
  212. self.shape_buffer = []
  213. self.move_timer = QtCore.QTimer()
  214. self.move_timer.setSingleShot(True)
  215. self.key = None # Currently pressed key
  216. def make_callback(tool):
  217. def f():
  218. self.on_tool_select(tool)
  219. return f
  220. for tool in self.tools:
  221. self.tools[tool]["button"].triggered.connect(make_callback(tool)) # Events
  222. self.tools[tool]["button"].setCheckable(True) # Checkable
  223. def clear(self):
  224. self.active_tool = None
  225. self.shape_buffer = []
  226. self.replot()
  227. def on_tool_select(self, tool):
  228. """
  229. :rtype : None
  230. """
  231. self.app.log.debug("on_tool_select('%s')" % tool)
  232. # This is to make the group behave as radio group
  233. if tool in self.tools:
  234. if self.tools[tool]["button"].isChecked():
  235. self.app.log.debug("%s is checked." % tool)
  236. for t in self.tools:
  237. if t != tool:
  238. self.tools[t]["button"].setChecked(False)
  239. self.active_tool = self.tools[tool]["constructor"](self)
  240. self.app.info(self.active_tool.start_msg)
  241. else:
  242. self.app.log.debug("%s is NOT checked." % tool)
  243. for t in self.tools:
  244. self.tools[t]["button"].setChecked(False)
  245. self.active_tool = None
  246. def on_canvas_click(self, event):
  247. """
  248. event.x .y have canvas coordinates
  249. event.xdaya .ydata have plot coordinates
  250. :param event:
  251. :return:
  252. """
  253. if self.active_tool is not None:
  254. # Dispatch event to active_tool
  255. msg = self.active_tool.click((event.xdata, event.ydata))
  256. self.app.info(msg)
  257. # If it is a shape generating tool
  258. if isinstance(self.active_tool, FCShapeTool) and self.active_tool.complete:
  259. self.on_shape_complete()
  260. return
  261. if isinstance(self.active_tool, FCSelect):
  262. self.app.log.debug("Replotting after click.")
  263. self.replot()
  264. else:
  265. self.app.log.debug("No active tool to respond to click!")
  266. def on_canvas_move(self, event):
  267. """
  268. event.x .y have canvas coordinates
  269. event.xdaya .ydata have plot coordinates
  270. :param event:
  271. :return:
  272. """
  273. self.on_canvas_move_effective(event)
  274. return
  275. self.move_timer.stop()
  276. if self.active_tool is None:
  277. return
  278. # Make a function to avoid late evaluation
  279. def make_callback():
  280. def f():
  281. self.on_canvas_move_effective(event)
  282. return f
  283. callback = make_callback()
  284. self.move_timer.timeout.connect(callback)
  285. self.move_timer.start(500) # Stops if aready running
  286. def on_canvas_move_effective(self, event):
  287. """
  288. Is called after timeout on timer set in on_canvas_move.
  289. For details on animating on MPL see:
  290. http://wiki.scipy.org/Cookbook/Matplotlib/Animations
  291. event.x .y have canvas coordinates
  292. event.xdaya .ydata have plot coordinates
  293. :param event:
  294. :return:
  295. """
  296. try:
  297. x = float(event.xdata)
  298. y = float(event.ydata)
  299. except TypeError:
  300. return
  301. if self.active_tool is None:
  302. return
  303. geo = self.active_tool.utility_geometry(data=(x, y))
  304. if geo is not None:
  305. # Remove any previous utility shape
  306. for shape in self.shape_buffer:
  307. if shape['utility']:
  308. self.shape_buffer.remove(shape)
  309. # Add the new utility shape
  310. self.shape_buffer.append({
  311. 'geometry': geo,
  312. 'selected': False,
  313. 'utility': True
  314. })
  315. # Efficient plotting for fast animation
  316. elements = self.plot_shape(geometry=geo, linespec="b--", animated=True)
  317. self.canvas.canvas.restore_region(self.canvas.background)
  318. for el in elements:
  319. self.axes.draw_artist(el)
  320. self.canvas.canvas.blit(self.axes.bbox)
  321. #self.replot()
  322. def on_canvas_key(self, event):
  323. """
  324. event.key has the key.
  325. :param event:
  326. :return:
  327. """
  328. self.key = event.key
  329. ### Finish the current action. Use with tools that do not
  330. ### complete automatically, like a polygon or path.
  331. if event.key == ' ':
  332. if isinstance(self.active_tool, FCShapeTool):
  333. self.active_tool.click((event.xdata, event.ydata))
  334. self.active_tool.make()
  335. if self.active_tool.complete:
  336. self.on_shape_complete()
  337. return
  338. ### Abort the current action
  339. if event.key == 'escape':
  340. # TODO: ...?
  341. self.on_tool_select("select")
  342. self.app.info("Cancelled.")
  343. for_deletion = [shape for shape in self.shape_buffer if shape['utility']]
  344. for shape in for_deletion:
  345. self.shape_buffer.remove(shape)
  346. self.replot()
  347. self.select_btn.setChecked(True)
  348. self.on_tool_select('select')
  349. return
  350. ### Delete selected object
  351. if event.key == '-':
  352. self.delete_selected()
  353. self.replot()
  354. ### Move
  355. if event.key == 'm':
  356. self.move_btn.setChecked(True)
  357. self.on_tool_select('move')
  358. self.active_tool.set_origin((event.xdata, event.ydata))
  359. ### Copy
  360. if event.key == 'c':
  361. self.copy_btn.setChecked(True)
  362. self.on_tool_select('copy')
  363. self.active_tool.set_origin((event.xdata, event.ydata))
  364. def on_canvas_key_release(self, event):
  365. self.key = None
  366. def get_selected(self):
  367. return [shape for shape in self.shape_buffer if shape["selected"]]
  368. def delete_selected(self):
  369. for shape in self.get_selected():
  370. self.shape_buffer.remove(shape)
  371. self.app.info("Shape deleted.")
  372. def plot_shape(self, geometry=None, linespec='b-', linewidth=1, animated=False):
  373. self.app.log.debug("plot_shape()")
  374. plot_elements = []
  375. if geometry is None:
  376. geometry = self.active_tool.geometry
  377. try:
  378. _ = iter(geometry)
  379. iterable_geometry = geometry
  380. except TypeError:
  381. iterable_geometry = [geometry]
  382. for geo in iterable_geometry:
  383. if type(geo) == Polygon:
  384. x, y = geo.exterior.coords.xy
  385. element, = self.axes.plot(x, y, linespec, linewidth=linewidth, animated=animated)
  386. plot_elements.append(element)
  387. for ints in geo.interiors:
  388. x, y = ints.coords.xy
  389. element, = self.axes.plot(x, y, linespec, linewidth=linewidth, animated=animated)
  390. plot_elements.append(element)
  391. continue
  392. if type(geo) == LineString or type(geo) == LinearRing:
  393. x, y = geo.coords.xy
  394. element, = self.axes.plot(x, y, linespec, linewidth=linewidth, animated=animated)
  395. plot_elements.append(element)
  396. continue
  397. if type(geo) == MultiPolygon:
  398. for poly in geo:
  399. x, y = poly.exterior.coords.xy
  400. element, = self.axes.plot(x, y, linespec, linewidth=linewidth, animated=animated)
  401. plot_elements.append(element)
  402. for ints in poly.interiors:
  403. x, y = ints.coords.xy
  404. element, = self.axes.plot(x, y, linespec, linewidth=linewidth, animated=animated)
  405. plot_elements.append(element)
  406. continue
  407. return plot_elements
  408. # self.canvas.auto_adjust_axes()
  409. def plot_all(self):
  410. self.app.log.debug("plot_all()")
  411. self.axes.cla()
  412. for shape in self.shape_buffer:
  413. if shape['utility']:
  414. self.plot_shape(geometry=shape['geometry'], linespec='k--', linewidth=1)
  415. continue
  416. if shape['selected']:
  417. self.plot_shape(geometry=shape['geometry'], linespec='k-', linewidth=2)
  418. continue
  419. self.plot_shape(geometry=shape['geometry'])
  420. self.canvas.auto_adjust_axes()
  421. def on_shape_complete(self):
  422. self.app.log.debug("on_shape_complete()")
  423. # For some reason plotting just the last created figure does not
  424. # work. The figure is not shown. Calling replot does the trick
  425. # which generates a new axes object.
  426. #self.plot_shape()
  427. #self.canvas.auto_adjust_axes()
  428. try:
  429. for geo in self.active_tool.geometry:
  430. self.shape_buffer.append({'geometry': geo,
  431. 'selected': False,
  432. 'utility': False})
  433. except TypeError:
  434. self.shape_buffer.append({'geometry': self.active_tool.geometry,
  435. 'selected': False,
  436. 'utility': False})
  437. # Remove any utility shapes
  438. for shape in self.shape_buffer:
  439. if shape['utility']:
  440. self.shape_buffer.remove(shape)
  441. self.replot()
  442. self.active_tool = type(self.active_tool)(self)
  443. def replot(self):
  444. #self.canvas.clear()
  445. self.axes = self.canvas.new_axes("draw")
  446. self.plot_all()
  447. def edit_fcgeometry(self, fcgeometry):
  448. """
  449. Imports the geometry from the given FlatCAM Geometry object
  450. into the editor.
  451. :param fcgeometry: FlatCAMGeometry
  452. :return: None
  453. """
  454. try:
  455. _ = iter(fcgeometry.solid_geometry)
  456. geometry = fcgeometry.solid_geometry
  457. except TypeError:
  458. geometry = [fcgeometry.solid_geometry]
  459. # Delete contents of editor.
  460. self.shape_buffer = []
  461. # Link shapes into editor.
  462. for shape in geometry:
  463. self.shape_buffer.append({'geometry': shape,
  464. 'selected': False,
  465. 'utility': False})
  466. self.replot()
  467. self.drawing_toolbar.setDisabled(False)
  468. def update_fcgeometry(self, fcgeometry):
  469. """
  470. Transfers the drawing tool shape buffer to the selected geometry
  471. object. The geometry already in the object are removed.
  472. :param fcgeometry: FlatCAMGeometry
  473. :return: None
  474. """
  475. fcgeometry.solid_geometry = []
  476. for shape in self.shape_buffer:
  477. fcgeometry.solid_geometry.append(shape['geometry'])
  478. def union(self):
  479. """
  480. Makes union of selected polygons. Original polygons
  481. are deleted.
  482. :return: None.
  483. """
  484. targets = [shape for shape in self.shape_buffer if shape['selected']]
  485. results = cascaded_union([t['geometry'] for t in targets])
  486. for shape in targets:
  487. self.shape_buffer.remove(shape)
  488. try:
  489. for geo in results:
  490. self.shape_buffer.append({
  491. 'geometry': geo,
  492. 'selected': True,
  493. 'utility': False
  494. })
  495. except TypeError:
  496. self.shape_buffer.append({
  497. 'geometry': results,
  498. 'selected': True,
  499. 'utility': False
  500. })
  501. self.replot()