FlatCAMDraw.py 33 KB

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