FlatCAMDraw.py 33 KB

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