FlatCAMDraw.py 42 KB

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