FlatCAMDraw.py 50 KB

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