FlatCAMDraw.py 42 KB

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