FlatCAMDraw.py 39 KB

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