FlatCAMDraw.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826
  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. from mpl_toolkits.axes_grid.anchored_artists import AnchoredDrawingArea
  13. from rtree import index as rtindex
  14. class DrawTool(object):
  15. """
  16. Abstract Class representing a tool in the drawing
  17. program. Can generate geometry, including temporary
  18. utility geometry that is updated on user clicks
  19. and mouse motion.
  20. """
  21. def __init__(self, draw_app):
  22. self.draw_app = draw_app
  23. self.complete = False
  24. self.start_msg = "Click on 1st point..."
  25. self.points = []
  26. self.geometry = None
  27. def click(self, point):
  28. return ""
  29. def utility_geometry(self, data=None):
  30. return None
  31. class FCShapeTool(DrawTool):
  32. def __init__(self, draw_app):
  33. DrawTool.__init__(self, draw_app)
  34. def make(self):
  35. pass
  36. class FCCircle(FCShapeTool):
  37. """
  38. Resulting type: Polygon
  39. """
  40. def __init__(self, draw_app):
  41. DrawTool.__init__(self, draw_app)
  42. self.start_msg = "Click on CENTER ..."
  43. def click(self, point):
  44. self.points.append(point)
  45. if len(self.points) == 1:
  46. return "Click on perimeter to complete ..."
  47. if len(self.points) == 2:
  48. self.make()
  49. return "Done."
  50. return ""
  51. def utility_geometry(self, data=None):
  52. if len(self.points) == 1:
  53. p1 = self.points[0]
  54. p2 = data
  55. radius = sqrt((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2)
  56. return Point(p1).buffer(radius)
  57. return None
  58. def make(self):
  59. p1 = self.points[0]
  60. p2 = self.points[1]
  61. radius = sqrt((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2)
  62. self.geometry = Point(p1).buffer(radius)
  63. self.complete = True
  64. class FCRectangle(FCShapeTool):
  65. """
  66. Resulting type: Polygon
  67. """
  68. def __init__(self, draw_app):
  69. DrawTool.__init__(self, draw_app)
  70. self.start_msg = "Click on 1st corner ..."
  71. def click(self, point):
  72. self.points.append(point)
  73. if len(self.points) == 1:
  74. return "Click on opposite corner to complete ..."
  75. if len(self.points) == 2:
  76. self.make()
  77. return "Done."
  78. return ""
  79. def utility_geometry(self, data=None):
  80. if len(self.points) == 1:
  81. p1 = self.points[0]
  82. p2 = data
  83. return LinearRing([p1, (p2[0], p1[1]), p2, (p1[0], p2[1])])
  84. return None
  85. def make(self):
  86. p1 = self.points[0]
  87. p2 = self.points[1]
  88. #self.geometry = LinearRing([p1, (p2[0], p1[1]), p2, (p1[0], p2[1])])
  89. self.geometry = Polygon([p1, (p2[0], p1[1]), p2, (p1[0], p2[1])])
  90. self.complete = True
  91. class FCPolygon(FCShapeTool):
  92. """
  93. Resulting type: Polygon
  94. """
  95. def __init__(self, draw_app):
  96. DrawTool.__init__(self, draw_app)
  97. self.start_msg = "Click on 1st point ..."
  98. def click(self, point):
  99. self.points.append(point)
  100. if len(self.points) > 0:
  101. return "Click on next point or hit SPACE to complete ..."
  102. return ""
  103. def utility_geometry(self, data=None):
  104. if len(self.points) == 1:
  105. temp_points = [x for x in self.points]
  106. temp_points.append(data)
  107. return LineString(temp_points)
  108. if len(self.points) > 1:
  109. temp_points = [x for x in self.points]
  110. temp_points.append(data)
  111. return LinearRing(temp_points)
  112. return None
  113. def make(self):
  114. # self.geometry = LinearRing(self.points)
  115. self.geometry = Polygon(self.points)
  116. self.complete = True
  117. class FCPath(FCPolygon):
  118. """
  119. Resulting type: LineString
  120. """
  121. def make(self):
  122. self.geometry = LineString(self.points)
  123. self.complete = True
  124. def utility_geometry(self, data=None):
  125. if len(self.points) > 1:
  126. temp_points = [x for x in self.points]
  127. temp_points.append(data)
  128. return LineString(temp_points)
  129. return None
  130. class FCSelect(DrawTool):
  131. def __init__(self, draw_app):
  132. DrawTool.__init__(self, draw_app)
  133. self.shape_buffer = self.draw_app.shape_buffer
  134. self.start_msg = "Click on geometry to select"
  135. def click(self, point):
  136. min_distance = Inf
  137. closest_shape = None
  138. for shape in self.shape_buffer:
  139. if self.draw_app.key != 'control':
  140. shape["selected"] = False
  141. # TODO: Do this with rtree?
  142. dist = Point(point).distance(shape["geometry"])
  143. if dist < min_distance:
  144. closest_shape = shape
  145. min_distance = dist
  146. if closest_shape is not None:
  147. closest_shape["selected"] = True
  148. return "Shape selected."
  149. return "Nothing selected."
  150. class FCMove(FCShapeTool):
  151. def __init__(self, draw_app):
  152. FCShapeTool.__init__(self, draw_app)
  153. self.shape_buffer = self.draw_app.shape_buffer
  154. self.origin = None
  155. self.destination = None
  156. self.start_msg = "Click on reference point."
  157. def set_origin(self, origin):
  158. self.origin = origin
  159. def click(self, point):
  160. if self.origin is None:
  161. self.set_origin(point)
  162. return "Click on final location."
  163. else:
  164. self.destination = point
  165. self.make()
  166. return "Done."
  167. def make(self):
  168. # Create new geometry
  169. dx = self.destination[0] - self.origin[0]
  170. dy = self.destination[1] - self.origin[1]
  171. self.geometry = [affinity.translate(geom['geometry'], xoff=dx, yoff=dy) for geom in self.draw_app.get_selected()]
  172. # Delete old
  173. for geo in self.draw_app.get_selected():
  174. self.draw_app.shape_buffer.remove(geo)
  175. self.complete = True
  176. def utility_geometry(self, data=None):
  177. """
  178. Temporary geometry on screen while using this tool.
  179. :param data:
  180. :return:
  181. """
  182. if self.origin is None:
  183. return None
  184. dx = data[0] - self.origin[0]
  185. dy = data[1] - self.origin[1]
  186. return [affinity.translate(geom['geometry'], xoff=dx, yoff=dy) for geom in self.draw_app.get_selected()]
  187. class FCCopy(FCMove):
  188. def make(self):
  189. # Create new geometry
  190. dx = self.destination[0] - self.origin[0]
  191. dy = self.destination[1] - self.origin[1]
  192. self.geometry = [affinity.translate(geom['geometry'], xoff=dx, yoff=dy) for geom in self.draw_app.get_selected()]
  193. self.complete = True
  194. ########################
  195. ### Main Application ###
  196. ########################
  197. class FlatCAMDraw(QtCore.QObject):
  198. def __init__(self, app, disabled=False):
  199. assert isinstance(app, FlatCAMApp.App)
  200. super(FlatCAMDraw, self).__init__()
  201. self.app = app
  202. self.canvas = app.plotcanvas
  203. self.axes = self.canvas.new_axes("draw")
  204. ### Drawing Toolbar ###
  205. self.drawing_toolbar = QtGui.QToolBar()
  206. self.drawing_toolbar.setDisabled(disabled)
  207. self.app.ui.addToolBar(self.drawing_toolbar)
  208. self.select_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/pointer32.png'), 'Select')
  209. self.add_circle_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/circle32.png'), 'Add Circle')
  210. self.add_rectangle_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/rectangle32.png'), 'Add Rectangle')
  211. self.add_polygon_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/polygon32.png'), 'Add Polygon')
  212. self.add_path_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/path32.png'), 'Add Path')
  213. self.union_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/union32.png'), 'Polygon Union')
  214. self.move_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/move32.png'), 'Move Objects')
  215. self.copy_btn = self.drawing_toolbar.addAction(QtGui.QIcon('share/copy32.png'), 'Copy Objects')
  216. ### Snap Toolbar ###
  217. self.snap_toolbar = QtGui.QToolBar()
  218. self.grid_snap_btn = self.snap_toolbar.addAction(QtGui.QIcon('share/grid32.png'), 'Snap to grid')
  219. self.grid_gap_x_entry = QtGui.QLineEdit()
  220. self.grid_gap_x_entry.setMaximumWidth(70)
  221. self.grid_gap_x_entry.setToolTip("Grid X distance")
  222. self.snap_toolbar.addWidget(self.grid_gap_x_entry)
  223. self.grid_gap_y_entry = QtGui.QLineEdit()
  224. self.grid_gap_y_entry.setMaximumWidth(70)
  225. self.grid_gap_y_entry.setToolTip("Grid Y distante")
  226. self.snap_toolbar.addWidget(self.grid_gap_y_entry)
  227. self.corner_snap_btn = self.snap_toolbar.addAction(QtGui.QIcon('share/corner32.png'), 'Snap to corner')
  228. self.snap_max_dist_entry = QtGui.QLineEdit()
  229. self.snap_max_dist_entry.setMaximumWidth(70)
  230. self.snap_max_dist_entry.setToolTip("Max. magnet distance")
  231. self.snap_toolbar.addWidget(self.snap_max_dist_entry)
  232. self.snap_toolbar.setDisabled(disabled)
  233. self.app.ui.addToolBar(self.snap_toolbar)
  234. ### Event handlers ###
  235. ## Canvas events
  236. self.canvas.mpl_connect('button_press_event', self.on_canvas_click)
  237. self.canvas.mpl_connect('motion_notify_event', self.on_canvas_move)
  238. self.canvas.mpl_connect('key_press_event', self.on_canvas_key)
  239. self.canvas.mpl_connect('key_release_event', self.on_canvas_key_release)
  240. self.union_btn.triggered.connect(self.union)
  241. ## Toolbar events and properties
  242. self.tools = {
  243. "select": {"button": self.select_btn,
  244. "constructor": FCSelect},
  245. "circle": {"button": self.add_circle_btn,
  246. "constructor": FCCircle},
  247. "rectangle": {"button": self.add_rectangle_btn,
  248. "constructor": FCRectangle},
  249. "polygon": {"button": self.add_polygon_btn,
  250. "constructor": FCPolygon},
  251. "path": {"button": self.add_path_btn,
  252. "constructor": FCPath},
  253. "move": {"button": self.move_btn,
  254. "constructor": FCMove},
  255. "copy": {"button": self.copy_btn,
  256. "constructor": FCCopy}
  257. }
  258. # Data
  259. self.active_tool = None
  260. self.shape_buffer = []
  261. self.move_timer = QtCore.QTimer()
  262. self.move_timer.setSingleShot(True)
  263. self.key = None # Currently pressed key
  264. def make_callback(tool):
  265. def f():
  266. self.on_tool_select(tool)
  267. return f
  268. for tool in self.tools:
  269. self.tools[tool]["button"].triggered.connect(make_callback(tool)) # Events
  270. self.tools[tool]["button"].setCheckable(True) # Checkable
  271. # for snap_tool in [self.grid_snap_btn, self.corner_snap_btn]:
  272. # snap_tool.triggered.connect(lambda: self.toolbar_tool_toggle("grid_snap"))
  273. # snap_tool.setCheckable(True)
  274. self.grid_snap_btn.setCheckable(True)
  275. self.grid_snap_btn.triggered.connect(lambda: self.toolbar_tool_toggle("grid_snap"))
  276. self.corner_snap_btn.setCheckable(True)
  277. self.corner_snap_btn.triggered.connect(lambda: self.toolbar_tool_toggle("corner_snap"))
  278. self.options = {
  279. "snap-x": 0.1,
  280. "snap-y": 0.1,
  281. "snap_max": 0.05,
  282. "grid_snap": False,
  283. "corner_snap": False,
  284. }
  285. self.grid_gap_x_entry.setText(str(self.options["snap-x"]))
  286. self.grid_gap_y_entry.setText(str(self.options["snap-y"]))
  287. self.snap_max_dist_entry.setText(str(self.options["snap_max"]))
  288. self.rtree_index = rtindex.Index()
  289. def entry2option(option, entry):
  290. self.options[option] = float(entry.text())
  291. self.grid_gap_x_entry.setValidator(QtGui.QDoubleValidator())
  292. self.grid_gap_x_entry.editingFinished.connect(lambda: entry2option("snap-x", self.grid_gap_x_entry))
  293. self.grid_gap_y_entry.setValidator(QtGui.QDoubleValidator())
  294. self.grid_gap_y_entry.editingFinished.connect(lambda: entry2option("snap-y", self.grid_gap_y_entry))
  295. self.snap_max_dist_entry.setValidator(QtGui.QDoubleValidator())
  296. self.snap_max_dist_entry.editingFinished.connect(lambda: entry2option("snap_max", self.snap_max_dist_entry))
  297. def activate(self):
  298. pass
  299. def deactivate(self):
  300. self.clear()
  301. self.drawing_toolbar.setDisabled(True)
  302. self.snap_toolbar.setDisabled(True) # TODO: Combine and move into tool
  303. def toolbar_tool_toggle(self, key):
  304. self.options[key] = self.sender().isChecked()
  305. print "grid_snap", self.options["grid_snap"]
  306. def clear(self):
  307. self.active_tool = None
  308. self.shape_buffer = []
  309. self.replot()
  310. def edit_fcgeometry(self, fcgeometry):
  311. """
  312. Imports the geometry from the given FlatCAM Geometry object
  313. into the editor.
  314. :param fcgeometry: FlatCAMGeometry
  315. :return: None
  316. """
  317. if fcgeometry.solid_geometry is None:
  318. geometry = []
  319. else:
  320. try:
  321. _ = iter(fcgeometry.solid_geometry)
  322. geometry = fcgeometry.solid_geometry
  323. except TypeError:
  324. geometry = [fcgeometry.solid_geometry]
  325. # Delete contents of editor.
  326. self.shape_buffer = []
  327. # Link shapes into editor.
  328. for shape in geometry:
  329. self.shape_buffer.append({'geometry': shape,
  330. 'selected': False,
  331. 'utility': False})
  332. self.replot()
  333. self.drawing_toolbar.setDisabled(False)
  334. self.snap_toolbar.setDisabled(False)
  335. def on_tool_select(self, tool):
  336. """
  337. Behavior of the toolbar. Tool initialization.
  338. :rtype : None
  339. """
  340. self.app.log.debug("on_tool_select('%s')" % tool)
  341. # This is to make the group behave as radio group
  342. if tool in self.tools:
  343. if self.tools[tool]["button"].isChecked():
  344. self.app.log.debug("%s is checked." % tool)
  345. for t in self.tools:
  346. if t != tool:
  347. self.tools[t]["button"].setChecked(False)
  348. self.active_tool = self.tools[tool]["constructor"](self)
  349. self.app.info(self.active_tool.start_msg)
  350. else:
  351. self.app.log.debug("%s is NOT checked." % tool)
  352. for t in self.tools:
  353. self.tools[t]["button"].setChecked(False)
  354. self.active_tool = None
  355. def on_canvas_click(self, event):
  356. """
  357. event.x .y have canvas coordinates
  358. event.xdaya .ydata have plot coordinates
  359. :param event:
  360. :return:
  361. """
  362. if self.active_tool is not None:
  363. # Dispatch event to active_tool
  364. msg = self.active_tool.click(self.snap(event.xdata, event.ydata))
  365. self.app.info(msg)
  366. # If it is a shape generating tool
  367. if isinstance(self.active_tool, FCShapeTool) and self.active_tool.complete:
  368. self.on_shape_complete()
  369. return
  370. if isinstance(self.active_tool, FCSelect):
  371. self.app.log.debug("Replotting after click.")
  372. self.replot()
  373. else:
  374. self.app.log.debug("No active tool to respond to click!")
  375. def on_canvas_move(self, event):
  376. """
  377. event.x .y have canvas coordinates
  378. event.xdaya .ydata have plot coordinates
  379. :param event:
  380. :return:
  381. """
  382. self.on_canvas_move_effective(event)
  383. return
  384. # self.move_timer.stop()
  385. #
  386. # if self.active_tool is None:
  387. # return
  388. #
  389. # # Make a function to avoid late evaluation
  390. # def make_callback():
  391. # def f():
  392. # self.on_canvas_move_effective(event)
  393. # return f
  394. # callback = make_callback()
  395. #
  396. # self.move_timer.timeout.connect(callback)
  397. # self.move_timer.start(500) # Stops if aready running
  398. def on_canvas_move_effective(self, event):
  399. """
  400. Is called after timeout on timer set in on_canvas_move.
  401. For details on animating on MPL see:
  402. http://wiki.scipy.org/Cookbook/Matplotlib/Animations
  403. event.x .y have canvas coordinates
  404. event.xdaya .ydata have plot coordinates
  405. :param event:
  406. :return:
  407. """
  408. try:
  409. x = float(event.xdata)
  410. y = float(event.ydata)
  411. except TypeError:
  412. return
  413. if self.active_tool is None:
  414. return
  415. x, y = self.snap(x, y)
  416. ### Utility geometry (animated)
  417. self.canvas.canvas.restore_region(self.canvas.background)
  418. geo = self.active_tool.utility_geometry(data=(x, y))
  419. if geo is not None and ((type(geo) == list and len(geo) > 0) or
  420. (type(geo) != list and not geo.is_empty)):
  421. # Remove any previous utility shape
  422. for shape in self.shape_buffer:
  423. if shape['utility']:
  424. self.shape_buffer.remove(shape)
  425. # Add the new utility shape
  426. self.shape_buffer.append({
  427. 'geometry': geo,
  428. 'selected': False,
  429. 'utility': True
  430. })
  431. # Efficient plotting for fast animation
  432. #self.canvas.canvas.restore_region(self.canvas.background)
  433. elements = self.plot_shape(geometry=geo, linespec="b--", animated=True)
  434. for el in elements:
  435. self.axes.draw_artist(el)
  436. #self.canvas.canvas.blit(self.axes.bbox)
  437. #self.replot()
  438. elements = self.axes.plot(x, y, 'bo', animated=True)
  439. for el in elements:
  440. self.axes.draw_artist(el)
  441. self.canvas.canvas.blit(self.axes.bbox)
  442. def on_canvas_key(self, event):
  443. """
  444. event.key has the key.
  445. :param event:
  446. :return:
  447. """
  448. self.key = event.key
  449. ### Finish the current action. Use with tools that do not
  450. ### complete automatically, like a polygon or path.
  451. if event.key == ' ':
  452. if isinstance(self.active_tool, FCShapeTool):
  453. self.active_tool.click(self.snap(event.xdata, event.ydata))
  454. self.active_tool.make()
  455. if self.active_tool.complete:
  456. self.on_shape_complete()
  457. return
  458. ### Abort the current action
  459. if event.key == 'escape':
  460. # TODO: ...?
  461. self.on_tool_select("select")
  462. self.app.info("Cancelled.")
  463. for_deletion = [shape for shape in self.shape_buffer if shape['utility']]
  464. for shape in for_deletion:
  465. self.shape_buffer.remove(shape)
  466. self.replot()
  467. self.select_btn.setChecked(True)
  468. self.on_tool_select('select')
  469. return
  470. ### Delete selected object
  471. if event.key == '-':
  472. self.delete_selected()
  473. self.replot()
  474. ### Move
  475. if event.key == 'm':
  476. self.move_btn.setChecked(True)
  477. self.on_tool_select('move')
  478. self.active_tool.set_origin(self.snap(event.xdata, event.ydata))
  479. ### Copy
  480. if event.key == 'c':
  481. self.copy_btn.setChecked(True)
  482. self.on_tool_select('copy')
  483. self.active_tool.set_origin(self.snap(event.xdata, event.ydata))
  484. ### Snap
  485. if event.key == 'g':
  486. self.grid_snap_btn.trigger()
  487. if event.key == 'k':
  488. self.corner_snap_btn.trigger()
  489. def on_canvas_key_release(self, event):
  490. self.key = None
  491. def get_selected(self):
  492. return [shape for shape in self.shape_buffer if shape["selected"]]
  493. def delete_selected(self):
  494. for shape in self.get_selected():
  495. self.shape_buffer.remove(shape)
  496. self.app.info("Shape deleted.")
  497. def plot_shape(self, geometry=None, linespec='b-', linewidth=1, animated=False):
  498. plot_elements = []
  499. if geometry is None:
  500. geometry = self.active_tool.geometry
  501. try:
  502. _ = iter(geometry)
  503. iterable_geometry = geometry
  504. except TypeError:
  505. iterable_geometry = [geometry]
  506. for geo in iterable_geometry:
  507. if type(geo) == Polygon:
  508. x, y = geo.exterior.coords.xy
  509. element, = self.axes.plot(x, y, linespec, linewidth=linewidth, animated=animated)
  510. plot_elements.append(element)
  511. for ints in geo.interiors:
  512. x, y = ints.coords.xy
  513. element, = self.axes.plot(x, y, linespec, linewidth=linewidth, animated=animated)
  514. plot_elements.append(element)
  515. continue
  516. if type(geo) == LineString or type(geo) == LinearRing:
  517. x, y = geo.coords.xy
  518. element, = self.axes.plot(x, y, linespec, linewidth=linewidth, animated=animated)
  519. plot_elements.append(element)
  520. continue
  521. if type(geo) == MultiPolygon:
  522. for poly in geo:
  523. x, y = poly.exterior.coords.xy
  524. element, = self.axes.plot(x, y, linespec, linewidth=linewidth, animated=animated)
  525. plot_elements.append(element)
  526. for ints in poly.interiors:
  527. x, y = ints.coords.xy
  528. element, = self.axes.plot(x, y, linespec, linewidth=linewidth, animated=animated)
  529. plot_elements.append(element)
  530. continue
  531. return plot_elements
  532. # self.canvas.auto_adjust_axes()
  533. def plot_all(self):
  534. self.app.log.debug("plot_all()")
  535. self.axes.cla()
  536. for shape in self.shape_buffer:
  537. if shape['geometry'] is None: # TODO: This shouldn't have happened
  538. continue
  539. if shape['utility']:
  540. self.plot_shape(geometry=shape['geometry'], linespec='k--', linewidth=1)
  541. continue
  542. if shape['selected']:
  543. self.plot_shape(geometry=shape['geometry'], linespec='k-', linewidth=2)
  544. continue
  545. self.plot_shape(geometry=shape['geometry'])
  546. self.canvas.auto_adjust_axes()
  547. def add2index(self, id, geo):
  548. try:
  549. for pt in geo.coords:
  550. self.rtree_index.add(id, pt)
  551. except NotImplementedError:
  552. # It's a polygon?
  553. for pt in geo.exterior.coords:
  554. self.rtree_index.add(id, pt)
  555. def on_shape_complete(self):
  556. self.app.log.debug("on_shape_complete()")
  557. # For some reason plotting just the last created figure does not
  558. # work. The figure is not shown. Calling replot does the trick
  559. # which generates a new axes object.
  560. #self.plot_shape()
  561. #self.canvas.auto_adjust_axes()
  562. try:
  563. for geo in self.active_tool.geometry:
  564. self.shape_buffer.append({'geometry': geo,
  565. 'selected': False,
  566. 'utility': False})
  567. self.add2index(len(self.shape_buffer)-1, geo)
  568. except TypeError:
  569. self.shape_buffer.append({'geometry': self.active_tool.geometry,
  570. 'selected': False,
  571. 'utility': False})
  572. self.add2index(len(self.shape_buffer)-1, self.active_tool.geometry)
  573. # Remove any utility shapes
  574. for shape in self.shape_buffer:
  575. if shape['utility']:
  576. self.shape_buffer.remove(shape)
  577. self.replot()
  578. self.active_tool = type(self.active_tool)(self)
  579. def replot(self):
  580. #self.canvas.clear()
  581. self.axes = self.canvas.new_axes("draw")
  582. self.plot_all()
  583. def snap(self, x, y):
  584. """
  585. Adjusts coordinates to snap settings.
  586. :param x: Input coordinate X
  587. :param y: Input coordinate Y
  588. :return: Snapped (x, y)
  589. """
  590. snap_x, snap_y = (x, y)
  591. snap_distance = Inf
  592. ### Object (corner?) snap
  593. if self.options["corner_snap"]:
  594. try:
  595. bbox = self.rtree_index.nearest((x, y), objects=True).next().bbox
  596. nearest_pt = (bbox[0], bbox[1])
  597. nearest_pt_distance = distance((x, y), nearest_pt)
  598. if nearest_pt_distance <= self.options["snap_max"]:
  599. snap_distance = nearest_pt_distance
  600. snap_x, snap_y = nearest_pt
  601. except StopIteration:
  602. pass
  603. ### Grid snap
  604. if self.options["grid_snap"]:
  605. if self.options["snap-x"] != 0:
  606. snap_x_ = round(x/self.options["snap-x"])*self.options['snap-x']
  607. else:
  608. snap_x_ = x
  609. if self.options["snap-y"] != 0:
  610. snap_y_ = round(y/self.options["snap-y"])*self.options['snap-y']
  611. else:
  612. snap_y_ = y
  613. nearest_grid_distance = distance((x, y), (snap_x_, snap_y_))
  614. if nearest_grid_distance < snap_distance:
  615. snap_x, snap_y = (snap_x_, snap_y_)
  616. return snap_x, snap_y
  617. def update_fcgeometry(self, fcgeometry):
  618. """
  619. Transfers the drawing tool shape buffer to the selected geometry
  620. object. The geometry already in the object are removed.
  621. :param fcgeometry: FlatCAMGeometry
  622. :return: None
  623. """
  624. fcgeometry.solid_geometry = []
  625. for shape in self.shape_buffer:
  626. fcgeometry.solid_geometry.append(shape['geometry'])
  627. def union(self):
  628. """
  629. Makes union of selected polygons. Original polygons
  630. are deleted.
  631. :return: None.
  632. """
  633. targets = [shape for shape in self.shape_buffer if shape['selected']]
  634. results = cascaded_union([t['geometry'] for t in targets])
  635. for shape in targets:
  636. self.shape_buffer.remove(shape)
  637. try:
  638. for geo in results:
  639. self.shape_buffer.append({
  640. 'geometry': geo,
  641. 'selected': True,
  642. 'utility': False
  643. })
  644. except TypeError:
  645. self.shape_buffer.append({
  646. 'geometry': results,
  647. 'selected': True,
  648. 'utility': False
  649. })
  650. self.replot()
  651. def distance(pt1, pt2):
  652. return sqrt((pt1[0] - pt2[0]) ** 2 + (pt1[1] - pt2[1]) ** 2)