FlatCAMDraw.py 45 KB

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