FlatCAMDraw.py 45 KB

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