FlatCAMDraw.py 26 KB

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