camlib.py 113 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358
  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 __future__ import division
  9. import traceback
  10. from numpy import arctan2, Inf, array, sqrt, pi, ceil, sin, cos, dot, float32, \
  11. transpose
  12. from numpy.linalg import solve, norm
  13. from matplotlib.figure import Figure
  14. import re
  15. import collections
  16. import numpy as np
  17. import matplotlib
  18. import matplotlib.pyplot as plt
  19. from scipy.spatial import Delaunay, KDTree
  20. from rtree import index as rtindex
  21. # See: http://toblerity.org/shapely/manual.html
  22. from shapely.geometry import Polygon, LineString, Point, LinearRing
  23. from shapely.geometry import MultiPoint, MultiPolygon
  24. from shapely.geometry import box as shply_box
  25. from shapely.ops import cascaded_union
  26. import shapely.affinity as affinity
  27. from shapely.wkt import loads as sloads
  28. from shapely.wkt import dumps as sdumps
  29. from shapely.geometry.base import BaseGeometry
  30. # Used for solid polygons in Matplotlib
  31. from descartes.patch import PolygonPatch
  32. import simplejson as json
  33. # TODO: Commented for FlatCAM packaging with cx_freeze
  34. #from matplotlib.pyplot import plot
  35. import logging
  36. log = logging.getLogger('base2')
  37. log.setLevel(logging.DEBUG)
  38. #log.setLevel(logging.WARNING)
  39. #log.setLevel(logging.INFO)
  40. formatter = logging.Formatter('[%(levelname)s] %(message)s')
  41. handler = logging.StreamHandler()
  42. handler.setFormatter(formatter)
  43. log.addHandler(handler)
  44. class Geometry(object):
  45. """
  46. Base geometry class.
  47. """
  48. defaults = {
  49. "init_units": 'in'
  50. }
  51. def __init__(self):
  52. # Units (in or mm)
  53. self.units = Geometry.defaults["init_units"]
  54. # Final geometry: MultiPolygon or list (of geometry constructs)
  55. self.solid_geometry = None
  56. # Attributes to be included in serialization
  57. self.ser_attrs = ['units', 'solid_geometry']
  58. # Flattened geometry (list of paths only)
  59. self.flat_geometry = []
  60. # Flat geometry rtree index
  61. self.flat_geometry_rtree = rtindex.Index()
  62. def add_circle(self, origin, radius):
  63. """
  64. Adds a circle to the object.
  65. :param origin: Center of the circle.
  66. :param radius: Radius of the circle.
  67. :return: None
  68. """
  69. # TODO: Decide what solid_geometry is supposed to be and how we append to it.
  70. if self.solid_geometry is None:
  71. self.solid_geometry = []
  72. if type(self.solid_geometry) is list:
  73. self.solid_geometry.append(Point(origin).buffer(radius))
  74. return
  75. try:
  76. self.solid_geometry = self.solid_geometry.union(Point(origin).buffer(radius))
  77. except:
  78. print "Failed to run union on polygons."
  79. raise
  80. def add_polygon(self, points):
  81. """
  82. Adds a polygon to the object (by union)
  83. :param points: The vertices of the polygon.
  84. :return: None
  85. """
  86. if self.solid_geometry is None:
  87. self.solid_geometry = []
  88. if type(self.solid_geometry) is list:
  89. self.solid_geometry.append(Polygon(points))
  90. return
  91. try:
  92. self.solid_geometry = self.solid_geometry.union(Polygon(points))
  93. except:
  94. print "Failed to run union on polygons."
  95. raise
  96. def bounds(self):
  97. """
  98. Returns coordinates of rectangular bounds
  99. of geometry: (xmin, ymin, xmax, ymax).
  100. """
  101. log.debug("Geometry->bounds()")
  102. if self.solid_geometry is None:
  103. log.debug("solid_geometry is None")
  104. return 0, 0, 0, 0
  105. if type(self.solid_geometry) is list:
  106. # TODO: This can be done faster. See comment from Shapely mailing lists.
  107. if len(self.solid_geometry) == 0:
  108. log.debug('solid_geometry is empty []')
  109. return 0, 0, 0, 0
  110. return cascaded_union(self.solid_geometry).bounds
  111. else:
  112. return self.solid_geometry.bounds
  113. def flatten(self, geometry=None, reset=True):
  114. if geometry is None:
  115. geometry = self.solid_geometry
  116. if reset:
  117. self.flat_geometry = []
  118. ## If iterable, expand recursively.
  119. try:
  120. for geo in geometry:
  121. self.flatten(geometry=geo, reset=False)
  122. ## Not iterable, do the actual indexing and add.
  123. except TypeError:
  124. if type(geometry) == Polygon:
  125. self.flat_geometry.append(geometry)
  126. return self.flat_geometry
  127. def make2Dindex(self):
  128. self.flatten()
  129. def get_pts(o):
  130. pts = []
  131. if type(o) == Polygon:
  132. g = o.exterior
  133. pts += list(g.coords)
  134. for i in o.interiors:
  135. pts += list(i.coords)
  136. else:
  137. pts += list(o.coords)
  138. return pts
  139. idx = FlatCAMRTreeStorage()
  140. idx.get_points = get_pts
  141. for shape in self.flat_geometry:
  142. idx.insert(shape)
  143. return idx
  144. def flatten_to_paths(self, geometry=None, reset=True):
  145. """
  146. Creates a list of non-iterable linear geometry elements and
  147. indexes them in rtree.
  148. :param geometry: Iterable geometry
  149. :param reset: Wether to clear (True) or append (False) to self.flat_geometry
  150. :return: self.flat_geometry, self.flat_geometry_rtree
  151. """
  152. if geometry is None:
  153. geometry = self.solid_geometry
  154. if reset:
  155. self.flat_geometry = []
  156. ## If iterable, expand recursively.
  157. try:
  158. for geo in geometry:
  159. self.flatten_to_paths(geometry=geo, reset=False)
  160. ## Not iterable, do the actual indexing and add.
  161. except TypeError:
  162. if type(geometry) == Polygon:
  163. g = geometry.exterior
  164. self.flat_geometry.append(g)
  165. ## Add first and last points of the path to the index.
  166. self.flat_geometry_rtree.insert(len(self.flat_geometry) - 1, g.coords[0])
  167. self.flat_geometry_rtree.insert(len(self.flat_geometry) - 1, g.coords[-1])
  168. for interior in geometry.interiors:
  169. g = interior
  170. self.flat_geometry.append(g)
  171. self.flat_geometry_rtree.insert(len(self.flat_geometry) - 1, g.coords[0])
  172. self.flat_geometry_rtree.insert(len(self.flat_geometry) - 1, g.coords[-1])
  173. else:
  174. g = geometry
  175. self.flat_geometry.append(g)
  176. self.flat_geometry_rtree.insert(len(self.flat_geometry) - 1, g.coords[0])
  177. self.flat_geometry_rtree.insert(len(self.flat_geometry) - 1, g.coords[-1])
  178. return self.flat_geometry, self.flat_geometry_rtree
  179. def isolation_geometry(self, offset):
  180. """
  181. Creates contours around geometry at a given
  182. offset distance.
  183. :param offset: Offset distance.
  184. :type offset: float
  185. :return: The buffered geometry.
  186. :rtype: Shapely.MultiPolygon or Shapely.Polygon
  187. """
  188. return self.solid_geometry.buffer(offset)
  189. def is_empty(self):
  190. if self.solid_geometry is None:
  191. return True
  192. if type(self.solid_geometry) is list and len(self.solid_geometry) == 0:
  193. return True
  194. return False
  195. def size(self):
  196. """
  197. Returns (width, height) of rectangular
  198. bounds of geometry.
  199. """
  200. if self.solid_geometry is None:
  201. log.warning("Solid_geometry not computed yet.")
  202. return 0
  203. bounds = self.bounds()
  204. return bounds[2] - bounds[0], bounds[3] - bounds[1]
  205. def get_empty_area(self, boundary=None):
  206. """
  207. Returns the complement of self.solid_geometry within
  208. the given boundary polygon. If not specified, it defaults to
  209. the rectangular bounding box of self.solid_geometry.
  210. """
  211. if boundary is None:
  212. boundary = self.solid_geometry.envelope
  213. return boundary.difference(self.solid_geometry)
  214. def clear_polygon(self, polygon, tooldia, overlap=0.15):
  215. """
  216. Creates geometry inside a polygon for a tool to cover
  217. the whole area.
  218. This algorithm shrinks the edges of the polygon and takes
  219. the resulting edges as toolpaths.
  220. :param polygon: Polygon to clear.
  221. :param tooldia: Diameter of the tool.
  222. :param overlap: Overlap of toolpasses.
  223. :return:
  224. """
  225. poly_cuts = [polygon.buffer(-tooldia/2.0)]
  226. while True:
  227. polygon = poly_cuts[-1].buffer(-tooldia*(1-overlap))
  228. if polygon.area > 0:
  229. poly_cuts.append(polygon)
  230. else:
  231. break
  232. return poly_cuts
  233. def clear_polygon2(self, polygon, tooldia, seedpoint=None, overlap=0.15):
  234. """
  235. Creates geometry inside a polygon for a tool to cover
  236. the whole area.
  237. This algorithm starts with a seed point inside the polygon
  238. and draws circles around it. Arcs inside the polygons are
  239. valid cuts. Finalizes by cutting around the inside edge of
  240. the polygon.
  241. :param polygon:
  242. :param tooldia:
  243. :param seedpoint:
  244. :param overlap:
  245. :return:
  246. """
  247. # Estimate good seedpoint if not provided.
  248. if seedpoint is None:
  249. seedpoint = polygon.representative_point()
  250. # Current buffer radius
  251. radius = tooldia / 2 * (1 - overlap)
  252. # The toolpaths
  253. geoms = []
  254. # Path margin
  255. path_margin = polygon.buffer(-tooldia / 2)
  256. # Grow from seed until outside the box.
  257. while 1:
  258. path = Point(seedpoint).buffer(radius).exterior
  259. path = path.intersection(path_margin)
  260. # Touches polygon?
  261. if path.is_empty:
  262. break
  263. else:
  264. geoms.append(path)
  265. radius += tooldia * (1 - overlap)
  266. # Clean edges
  267. outer_edges = [x.exterior for x in autolist(polygon.buffer(-tooldia / 2))]
  268. inner_edges = []
  269. for x in autolist(polygon.buffer(-tooldia / 2)): # Over resulting polygons
  270. for y in x.interiors: # Over interiors of each polygon
  271. inner_edges.append(y)
  272. geoms += outer_edges + inner_edges
  273. return geoms
  274. def scale(self, factor):
  275. """
  276. Scales all of the object's geometry by a given factor. Override
  277. this method.
  278. :param factor: Number by which to scale.
  279. :type factor: float
  280. :return: None
  281. :rtype: None
  282. """
  283. return
  284. def offset(self, vect):
  285. """
  286. Offset the geometry by the given vector. Override this method.
  287. :param vect: (x, y) vector by which to offset the object.
  288. :type vect: tuple
  289. :return: None
  290. """
  291. return
  292. def convert_units(self, units):
  293. """
  294. Converts the units of the object to ``units`` by scaling all
  295. the geometry appropriately. This call ``scale()``. Don't call
  296. it again in descendents.
  297. :param units: "IN" or "MM"
  298. :type units: str
  299. :return: Scaling factor resulting from unit change.
  300. :rtype: float
  301. """
  302. log.debug("Geometry.convert_units()")
  303. if units.upper() == self.units.upper():
  304. return 1.0
  305. if units.upper() == "MM":
  306. factor = 25.4
  307. elif units.upper() == "IN":
  308. factor = 1/25.4
  309. else:
  310. log.error("Unsupported units: %s" % str(units))
  311. return 1.0
  312. self.units = units
  313. self.scale(factor)
  314. return factor
  315. def to_dict(self):
  316. """
  317. Returns a respresentation of the object as a dictionary.
  318. Attributes to include are listed in ``self.ser_attrs``.
  319. :return: A dictionary-encoded copy of the object.
  320. :rtype: dict
  321. """
  322. d = {}
  323. for attr in self.ser_attrs:
  324. d[attr] = getattr(self, attr)
  325. return d
  326. def from_dict(self, d):
  327. """
  328. Sets object's attributes from a dictionary.
  329. Attributes to include are listed in ``self.ser_attrs``.
  330. This method will look only for only and all the
  331. attributes in ``self.ser_attrs``. They must all
  332. be present. Use only for deserializing saved
  333. objects.
  334. :param d: Dictionary of attributes to set in the object.
  335. :type d: dict
  336. :return: None
  337. """
  338. for attr in self.ser_attrs:
  339. setattr(self, attr, d[attr])
  340. def union(self):
  341. """
  342. Runs a cascaded union on the list of objects in
  343. solid_geometry.
  344. :return: None
  345. """
  346. self.solid_geometry = [cascaded_union(self.solid_geometry)]
  347. class ApertureMacro:
  348. """
  349. Syntax of aperture macros.
  350. <AM command>: AM<Aperture macro name>*<Macro content>
  351. <Macro content>: {{<Variable definition>*}{<Primitive>*}}
  352. <Variable definition>: $K=<Arithmetic expression>
  353. <Primitive>: <Primitive code>,<Modifier>{,<Modifier>}|<Comment>
  354. <Modifier>: $M|< Arithmetic expression>
  355. <Comment>: 0 <Text>
  356. """
  357. ## Regular expressions
  358. am1_re = re.compile(r'^%AM([^\*]+)\*(.+)?(%)?$')
  359. am2_re = re.compile(r'(.*)%$')
  360. amcomm_re = re.compile(r'^0(.*)')
  361. amprim_re = re.compile(r'^[1-9].*')
  362. amvar_re = re.compile(r'^\$([0-9a-zA-z]+)=(.*)')
  363. def __init__(self, name=None):
  364. self.name = name
  365. self.raw = ""
  366. ## These below are recomputed for every aperture
  367. ## definition, in other words, are temporary variables.
  368. self.primitives = []
  369. self.locvars = {}
  370. self.geometry = None
  371. def to_dict(self):
  372. """
  373. Returns the object in a serializable form. Only the name and
  374. raw are required.
  375. :return: Dictionary representing the object. JSON ready.
  376. :rtype: dict
  377. """
  378. return {
  379. 'name': self.name,
  380. 'raw': self.raw
  381. }
  382. def from_dict(self, d):
  383. """
  384. Populates the object from a serial representation created
  385. with ``self.to_dict()``.
  386. :param d: Serial representation of an ApertureMacro object.
  387. :return: None
  388. """
  389. for attr in ['name', 'raw']:
  390. setattr(self, attr, d[attr])
  391. def parse_content(self):
  392. """
  393. Creates numerical lists for all primitives in the aperture
  394. macro (in ``self.raw``) by replacing all variables by their
  395. values iteratively and evaluating expressions. Results
  396. are stored in ``self.primitives``.
  397. :return: None
  398. """
  399. # Cleanup
  400. self.raw = self.raw.replace('\n', '').replace('\r', '').strip(" *")
  401. self.primitives = []
  402. # Separate parts
  403. parts = self.raw.split('*')
  404. #### Every part in the macro ####
  405. for part in parts:
  406. ### Comments. Ignored.
  407. match = ApertureMacro.amcomm_re.search(part)
  408. if match:
  409. continue
  410. ### Variables
  411. # These are variables defined locally inside the macro. They can be
  412. # numerical constant or defind in terms of previously define
  413. # variables, which can be defined locally or in an aperture
  414. # definition. All replacements ocurr here.
  415. match = ApertureMacro.amvar_re.search(part)
  416. if match:
  417. var = match.group(1)
  418. val = match.group(2)
  419. # Replace variables in value
  420. for v in self.locvars:
  421. val = re.sub(r'\$'+str(v)+r'(?![0-9a-zA-Z])', str(self.locvars[v]), val)
  422. # Make all others 0
  423. val = re.sub(r'\$[0-9a-zA-Z](?![0-9a-zA-Z])', "0", val)
  424. # Change x with *
  425. val = re.sub(r'[xX]', "*", val)
  426. # Eval() and store.
  427. self.locvars[var] = eval(val)
  428. continue
  429. ### Primitives
  430. # Each is an array. The first identifies the primitive, while the
  431. # rest depend on the primitive. All are strings representing a
  432. # number and may contain variable definition. The values of these
  433. # variables are defined in an aperture definition.
  434. match = ApertureMacro.amprim_re.search(part)
  435. if match:
  436. ## Replace all variables
  437. for v in self.locvars:
  438. part = re.sub(r'\$'+str(v)+r'(?![0-9a-zA-Z])', str(self.locvars[v]), part)
  439. # Make all others 0
  440. part = re.sub(r'\$[0-9a-zA-Z](?![0-9a-zA-Z])', "0", part)
  441. # Change x with *
  442. part = re.sub(r'[xX]', "*", part)
  443. ## Store
  444. elements = part.split(",")
  445. self.primitives.append([eval(x) for x in elements])
  446. continue
  447. log.warning("Unknown syntax of aperture macro part: %s" % str(part))
  448. def append(self, data):
  449. """
  450. Appends a string to the raw macro.
  451. :param data: Part of the macro.
  452. :type data: str
  453. :return: None
  454. """
  455. self.raw += data
  456. @staticmethod
  457. def default2zero(n, mods):
  458. """
  459. Pads the ``mods`` list with zeros resulting in an
  460. list of length n.
  461. :param n: Length of the resulting list.
  462. :type n: int
  463. :param mods: List to be padded.
  464. :type mods: list
  465. :return: Zero-padded list.
  466. :rtype: list
  467. """
  468. x = [0.0]*n
  469. na = len(mods)
  470. x[0:na] = mods
  471. return x
  472. @staticmethod
  473. def make_circle(mods):
  474. """
  475. :param mods: (Exposure 0/1, Diameter >=0, X-coord, Y-coord)
  476. :return:
  477. """
  478. pol, dia, x, y = ApertureMacro.default2zero(4, mods)
  479. return {"pol": int(pol), "geometry": Point(x, y).buffer(dia/2)}
  480. @staticmethod
  481. def make_vectorline(mods):
  482. """
  483. :param mods: (Exposure 0/1, Line width >= 0, X-start, Y-start, X-end, Y-end,
  484. rotation angle around origin in degrees)
  485. :return:
  486. """
  487. pol, width, xs, ys, xe, ye, angle = ApertureMacro.default2zero(7, mods)
  488. line = LineString([(xs, ys), (xe, ye)])
  489. box = line.buffer(width/2, cap_style=2)
  490. box_rotated = affinity.rotate(box, angle, origin=(0, 0))
  491. return {"pol": int(pol), "geometry": box_rotated}
  492. @staticmethod
  493. def make_centerline(mods):
  494. """
  495. :param mods: (Exposure 0/1, width >=0, height >=0, x-center, y-center,
  496. rotation angle around origin in degrees)
  497. :return:
  498. """
  499. pol, width, height, x, y, angle = ApertureMacro.default2zero(6, mods)
  500. box = shply_box(x-width/2, y-height/2, x+width/2, y+height/2)
  501. box_rotated = affinity.rotate(box, angle, origin=(0, 0))
  502. return {"pol": int(pol), "geometry": box_rotated}
  503. @staticmethod
  504. def make_lowerleftline(mods):
  505. """
  506. :param mods: (exposure 0/1, width >=0, height >=0, x-lowerleft, y-lowerleft,
  507. rotation angle around origin in degrees)
  508. :return:
  509. """
  510. pol, width, height, x, y, angle = ApertureMacro.default2zero(6, mods)
  511. box = shply_box(x, y, x+width, y+height)
  512. box_rotated = affinity.rotate(box, angle, origin=(0, 0))
  513. return {"pol": int(pol), "geometry": box_rotated}
  514. @staticmethod
  515. def make_outline(mods):
  516. """
  517. :param mods:
  518. :return:
  519. """
  520. pol = mods[0]
  521. n = mods[1]
  522. points = [(0, 0)]*(n+1)
  523. for i in range(n+1):
  524. points[i] = mods[2*i + 2:2*i + 4]
  525. angle = mods[2*n + 4]
  526. poly = Polygon(points)
  527. poly_rotated = affinity.rotate(poly, angle, origin=(0, 0))
  528. return {"pol": int(pol), "geometry": poly_rotated}
  529. @staticmethod
  530. def make_polygon(mods):
  531. """
  532. Note: Specs indicate that rotation is only allowed if the center
  533. (x, y) == (0, 0). I will tolerate breaking this rule.
  534. :param mods: (exposure 0/1, n_verts 3<=n<=12, x-center, y-center,
  535. diameter of circumscribed circle >=0, rotation angle around origin)
  536. :return:
  537. """
  538. pol, nverts, x, y, dia, angle = ApertureMacro.default2zero(6, mods)
  539. points = [(0, 0)]*nverts
  540. for i in range(nverts):
  541. points[i] = (x + 0.5 * dia * cos(2*pi * i/nverts),
  542. y + 0.5 * dia * sin(2*pi * i/nverts))
  543. poly = Polygon(points)
  544. poly_rotated = affinity.rotate(poly, angle, origin=(0, 0))
  545. return {"pol": int(pol), "geometry": poly_rotated}
  546. @staticmethod
  547. def make_moire(mods):
  548. """
  549. Note: Specs indicate that rotation is only allowed if the center
  550. (x, y) == (0, 0). I will tolerate breaking this rule.
  551. :param mods: (x-center, y-center, outer_dia_outer_ring, ring thickness,
  552. gap, max_rings, crosshair_thickness, crosshair_len, rotation
  553. angle around origin in degrees)
  554. :return:
  555. """
  556. x, y, dia, thickness, gap, nrings, cross_th, cross_len, angle = ApertureMacro.default2zero(9, mods)
  557. r = dia/2 - thickness/2
  558. result = Point((x, y)).buffer(r).exterior.buffer(thickness/2.0)
  559. ring = Point((x, y)).buffer(r).exterior.buffer(thickness/2.0) # Need a copy!
  560. i = 1 # Number of rings created so far
  561. ## If the ring does not have an interior it means that it is
  562. ## a disk. Then stop.
  563. while len(ring.interiors) > 0 and i < nrings:
  564. r -= thickness + gap
  565. if r <= 0:
  566. break
  567. ring = Point((x, y)).buffer(r).exterior.buffer(thickness/2.0)
  568. result = cascaded_union([result, ring])
  569. i += 1
  570. ## Crosshair
  571. hor = LineString([(x - cross_len, y), (x + cross_len, y)]).buffer(cross_th/2.0, cap_style=2)
  572. ver = LineString([(x, y-cross_len), (x, y + cross_len)]).buffer(cross_th/2.0, cap_style=2)
  573. result = cascaded_union([result, hor, ver])
  574. return {"pol": 1, "geometry": result}
  575. @staticmethod
  576. def make_thermal(mods):
  577. """
  578. Note: Specs indicate that rotation is only allowed if the center
  579. (x, y) == (0, 0). I will tolerate breaking this rule.
  580. :param mods: [x-center, y-center, diameter-outside, diameter-inside,
  581. gap-thickness, rotation angle around origin]
  582. :return:
  583. """
  584. x, y, dout, din, t, angle = ApertureMacro.default2zero(6, mods)
  585. ring = Point((x, y)).buffer(dout/2.0).difference(Point((x, y)).buffer(din/2.0))
  586. hline = LineString([(x - dout/2.0, y), (x + dout/2.0, y)]).buffer(t/2.0, cap_style=3)
  587. vline = LineString([(x, y - dout/2.0), (x, y + dout/2.0)]).buffer(t/2.0, cap_style=3)
  588. thermal = ring.difference(hline.union(vline))
  589. return {"pol": 1, "geometry": thermal}
  590. def make_geometry(self, modifiers):
  591. """
  592. Runs the macro for the given modifiers and generates
  593. the corresponding geometry.
  594. :param modifiers: Modifiers (parameters) for this macro
  595. :type modifiers: list
  596. """
  597. ## Primitive makers
  598. makers = {
  599. "1": ApertureMacro.make_circle,
  600. "2": ApertureMacro.make_vectorline,
  601. "20": ApertureMacro.make_vectorline,
  602. "21": ApertureMacro.make_centerline,
  603. "22": ApertureMacro.make_lowerleftline,
  604. "4": ApertureMacro.make_outline,
  605. "5": ApertureMacro.make_polygon,
  606. "6": ApertureMacro.make_moire,
  607. "7": ApertureMacro.make_thermal
  608. }
  609. ## Store modifiers as local variables
  610. modifiers = modifiers or []
  611. modifiers = [float(m) for m in modifiers]
  612. self.locvars = {}
  613. for i in range(0, len(modifiers)):
  614. self.locvars[str(i+1)] = modifiers[i]
  615. ## Parse
  616. self.primitives = [] # Cleanup
  617. self.geometry = None
  618. self.parse_content()
  619. ## Make the geometry
  620. for primitive in self.primitives:
  621. # Make the primitive
  622. prim_geo = makers[str(int(primitive[0]))](primitive[1:])
  623. # Add it (according to polarity)
  624. if self.geometry is None and prim_geo['pol'] == 1:
  625. self.geometry = prim_geo['geometry']
  626. continue
  627. if prim_geo['pol'] == 1:
  628. self.geometry = self.geometry.union(prim_geo['geometry'])
  629. continue
  630. if prim_geo['pol'] == 0:
  631. self.geometry = self.geometry.difference(prim_geo['geometry'])
  632. continue
  633. return self.geometry
  634. class Gerber (Geometry):
  635. """
  636. **ATTRIBUTES**
  637. * ``apertures`` (dict): The keys are names/identifiers of each aperture.
  638. The values are dictionaries key/value pairs which describe the aperture. The
  639. type key is always present and the rest depend on the key:
  640. +-----------+-----------------------------------+
  641. | Key | Value |
  642. +===========+===================================+
  643. | type | (str) "C", "R", "O", "P", or "AP" |
  644. +-----------+-----------------------------------+
  645. | others | Depend on ``type`` |
  646. +-----------+-----------------------------------+
  647. * ``aperture_macros`` (dictionary): Are predefined geometrical structures
  648. that can be instanciated with different parameters in an aperture
  649. definition. See ``apertures`` above. The key is the name of the macro,
  650. and the macro itself, the value, is a ``Aperture_Macro`` object.
  651. * ``flash_geometry`` (list): List of (Shapely) geometric object resulting
  652. from ``flashes``. These are generated from ``flashes`` in ``do_flashes()``.
  653. * ``buffered_paths`` (list): List of (Shapely) polygons resulting from
  654. *buffering* (or thickening) the ``paths`` with the aperture. These are
  655. generated from ``paths`` in ``buffer_paths()``.
  656. **USAGE**::
  657. g = Gerber()
  658. g.parse_file(filename)
  659. g.create_geometry()
  660. do_something(s.solid_geometry)
  661. """
  662. defaults = {
  663. "steps_per_circle": 40
  664. }
  665. def __init__(self, steps_per_circle=None):
  666. """
  667. The constructor takes no parameters. Use ``gerber.parse_files()``
  668. or ``gerber.parse_lines()`` to populate the object from Gerber source.
  669. :return: Gerber object
  670. :rtype: Gerber
  671. """
  672. # Initialize parent
  673. Geometry.__init__(self)
  674. self.solid_geometry = Polygon()
  675. # Number format
  676. self.int_digits = 3
  677. """Number of integer digits in Gerber numbers. Used during parsing."""
  678. self.frac_digits = 4
  679. """Number of fraction digits in Gerber numbers. Used during parsing."""
  680. ## Gerber elements ##
  681. # Apertures {'id':{'type':chr,
  682. # ['size':float], ['width':float],
  683. # ['height':float]}, ...}
  684. self.apertures = {}
  685. # Aperture Macros
  686. self.aperture_macros = {}
  687. # Attributes to be included in serialization
  688. # Always append to it because it carries contents
  689. # from Geometry.
  690. self.ser_attrs += ['int_digits', 'frac_digits', 'apertures',
  691. 'aperture_macros', 'solid_geometry']
  692. #### Parser patterns ####
  693. # FS - Format Specification
  694. # The format of X and Y must be the same!
  695. # L-omit leading zeros, T-omit trailing zeros
  696. # A-absolute notation, I-incremental notation
  697. self.fmt_re = re.compile(r'%FS([LT])([AI])X(\d)(\d)Y\d\d\*%$')
  698. # Mode (IN/MM)
  699. self.mode_re = re.compile(r'^%MO(IN|MM)\*%$')
  700. # Comment G04|G4
  701. self.comm_re = re.compile(r'^G0?4(.*)$')
  702. # AD - Aperture definition
  703. self.ad_re = re.compile(r'^%ADD(\d\d+)([a-zA-Z_$\.][a-zA-Z0-9_$\.]*)(?:,(.*))?\*%$')
  704. # AM - Aperture Macro
  705. # Beginning of macro (Ends with *%):
  706. #self.am_re = re.compile(r'^%AM([a-zA-Z0-9]*)\*')
  707. # Tool change
  708. # May begin with G54 but that is deprecated
  709. self.tool_re = re.compile(r'^(?:G54)?D(\d\d+)\*$')
  710. # G01... - Linear interpolation plus flashes with coordinates
  711. # Operation code (D0x) missing is deprecated... oh well I will support it.
  712. self.lin_re = re.compile(r'^(?:G0?(1))?(?=.*X(-?\d+))?(?=.*Y(-?\d+))?[XY][^DIJ]*(?:D0?([123]))?\*$')
  713. # Operation code alone, usually just D03 (Flash)
  714. self.opcode_re = re.compile(r'^D0?([123])\*$')
  715. # G02/3... - Circular interpolation with coordinates
  716. # 2-clockwise, 3-counterclockwise
  717. # Operation code (D0x) missing is deprecated... oh well I will support it.
  718. # Optional start with G02 or G03, optional end with D01 or D02 with
  719. # optional coordinates but at least one in any order.
  720. self.circ_re = re.compile(r'^(?:G0?([23]))?(?=.*X(-?\d+))?(?=.*Y(-?\d+))' +
  721. '?(?=.*I(-?\d+))?(?=.*J(-?\d+))?[XYIJ][^D]*(?:D0([12]))?\*$')
  722. # G01/2/3 Occurring without coordinates
  723. self.interp_re = re.compile(r'^(?:G0?([123]))\*')
  724. # Single D74 or multi D75 quadrant for circular interpolation
  725. self.quad_re = re.compile(r'^G7([45])\*$')
  726. # Region mode on
  727. # In region mode, D01 starts a region
  728. # and D02 ends it. A new region can be started again
  729. # with D01. All contours must be closed before
  730. # D02 or G37.
  731. self.regionon_re = re.compile(r'^G36\*$')
  732. # Region mode off
  733. # Will end a region and come off region mode.
  734. # All contours must be closed before D02 or G37.
  735. self.regionoff_re = re.compile(r'^G37\*$')
  736. # End of file
  737. self.eof_re = re.compile(r'^M02\*')
  738. # IP - Image polarity
  739. self.pol_re = re.compile(r'^%IP(POS|NEG)\*%$')
  740. # LP - Level polarity
  741. self.lpol_re = re.compile(r'^%LP([DC])\*%$')
  742. # Units (OBSOLETE)
  743. self.units_re = re.compile(r'^G7([01])\*$')
  744. # Absolute/Relative G90/1 (OBSOLETE)
  745. self.absrel_re = re.compile(r'^G9([01])\*$')
  746. # Aperture macros
  747. self.am1_re = re.compile(r'^%AM([^\*]+)\*([^%]+)?(%)?$')
  748. self.am2_re = re.compile(r'(.*)%$')
  749. # How to discretize a circle.
  750. self.steps_per_circ = steps_per_circle or Gerber.defaults['steps_per_circle']
  751. def scale(self, factor):
  752. """
  753. Scales the objects' geometry on the XY plane by a given factor.
  754. These are:
  755. * ``buffered_paths``
  756. * ``flash_geometry``
  757. * ``solid_geometry``
  758. * ``regions``
  759. NOTE:
  760. Does not modify the data used to create these elements. If these
  761. are recreated, the scaling will be lost. This behavior was modified
  762. because of the complexity reached in this class.
  763. :param factor: Number by which to scale.
  764. :type factor: float
  765. :rtype : None
  766. """
  767. ## solid_geometry ???
  768. # It's a cascaded union of objects.
  769. self.solid_geometry = affinity.scale(self.solid_geometry, factor,
  770. factor, origin=(0, 0))
  771. # # Now buffered_paths, flash_geometry and solid_geometry
  772. # self.create_geometry()
  773. def offset(self, vect):
  774. """
  775. Offsets the objects' geometry on the XY plane by a given vector.
  776. These are:
  777. * ``buffered_paths``
  778. * ``flash_geometry``
  779. * ``solid_geometry``
  780. * ``regions``
  781. NOTE:
  782. Does not modify the data used to create these elements. If these
  783. are recreated, the scaling will be lost. This behavior was modified
  784. because of the complexity reached in this class.
  785. :param vect: (x, y) offset vector.
  786. :type vect: tuple
  787. :return: None
  788. """
  789. dx, dy = vect
  790. ## Solid geometry
  791. self.solid_geometry = affinity.translate(self.solid_geometry, xoff=dx, yoff=dy)
  792. def mirror(self, axis, point):
  793. """
  794. Mirrors the object around a specified axis passign through
  795. the given point. What is affected:
  796. * ``buffered_paths``
  797. * ``flash_geometry``
  798. * ``solid_geometry``
  799. * ``regions``
  800. NOTE:
  801. Does not modify the data used to create these elements. If these
  802. are recreated, the scaling will be lost. This behavior was modified
  803. because of the complexity reached in this class.
  804. :param axis: "X" or "Y" indicates around which axis to mirror.
  805. :type axis: str
  806. :param point: [x, y] point belonging to the mirror axis.
  807. :type point: list
  808. :return: None
  809. """
  810. px, py = point
  811. xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
  812. ## solid_geometry ???
  813. # It's a cascaded union of objects.
  814. self.solid_geometry = affinity.scale(self.solid_geometry,
  815. xscale, yscale, origin=(px, py))
  816. def aperture_parse(self, apertureId, apertureType, apParameters):
  817. """
  818. Parse gerber aperture definition into dictionary of apertures.
  819. The following kinds and their attributes are supported:
  820. * *Circular (C)*: size (float)
  821. * *Rectangle (R)*: width (float), height (float)
  822. * *Obround (O)*: width (float), height (float).
  823. * *Polygon (P)*: diameter(float), vertices(int), [rotation(float)]
  824. * *Aperture Macro (AM)*: macro (ApertureMacro), modifiers (list)
  825. :param apertureId: Id of the aperture being defined.
  826. :param apertureType: Type of the aperture.
  827. :param apParameters: Parameters of the aperture.
  828. :type apertureId: str
  829. :type apertureType: str
  830. :type apParameters: str
  831. :return: Identifier of the aperture.
  832. :rtype: str
  833. """
  834. # Found some Gerber with a leading zero in the aperture id and the
  835. # referenced it without the zero, so this is a hack to handle that.
  836. apid = str(int(apertureId))
  837. try: # Could be empty for aperture macros
  838. paramList = apParameters.split('X')
  839. except:
  840. paramList = None
  841. if apertureType == "C": # Circle, example: %ADD11C,0.1*%
  842. self.apertures[apid] = {"type": "C",
  843. "size": float(paramList[0])}
  844. return apid
  845. if apertureType == "R": # Rectangle, example: %ADD15R,0.05X0.12*%
  846. self.apertures[apid] = {"type": "R",
  847. "width": float(paramList[0]),
  848. "height": float(paramList[1]),
  849. "size": sqrt(float(paramList[0])**2 + float(paramList[1])**2)} # Hack
  850. return apid
  851. if apertureType == "O": # Obround
  852. self.apertures[apid] = {"type": "O",
  853. "width": float(paramList[0]),
  854. "height": float(paramList[1]),
  855. "size": sqrt(float(paramList[0])**2 + float(paramList[1])**2)} # Hack
  856. return apid
  857. if apertureType == "P": # Polygon (regular)
  858. self.apertures[apid] = {"type": "P",
  859. "diam": float(paramList[0]),
  860. "nVertices": int(paramList[1]),
  861. "size": float(paramList[0])} # Hack
  862. if len(paramList) >= 3:
  863. self.apertures[apid]["rotation"] = float(paramList[2])
  864. return apid
  865. if apertureType in self.aperture_macros:
  866. self.apertures[apid] = {"type": "AM",
  867. "macro": self.aperture_macros[apertureType],
  868. "modifiers": paramList}
  869. return apid
  870. log.warning("Aperture not implemented: %s" % str(apertureType))
  871. return None
  872. def parse_file(self, filename, follow=False):
  873. """
  874. Calls Gerber.parse_lines() with array of lines
  875. read from the given file.
  876. :param filename: Gerber file to parse.
  877. :type filename: str
  878. :param follow: If true, will not create polygons, just lines
  879. following the gerber path.
  880. :type follow: bool
  881. :return: None
  882. """
  883. gfile = open(filename, 'r')
  884. gstr = gfile.readlines()
  885. gfile.close()
  886. self.parse_lines(gstr, follow=follow)
  887. def parse_lines(self, glines, follow=False):
  888. """
  889. Main Gerber parser. Reads Gerber and populates ``self.paths``, ``self.apertures``,
  890. ``self.flashes``, ``self.regions`` and ``self.units``.
  891. :param glines: Gerber code as list of strings, each element being
  892. one line of the source file.
  893. :type glines: list
  894. :param follow: If true, will not create polygons, just lines
  895. following the gerber path.
  896. :type follow: bool
  897. :return: None
  898. :rtype: None
  899. """
  900. # Coordinates of the current path, each is [x, y]
  901. path = []
  902. # Polygons are stored here until there is a change in polarity.
  903. # Only then they are combined via cascaded_union and added or
  904. # subtracted from solid_geometry. This is ~100 times faster than
  905. # applyng a union for every new polygon.
  906. poly_buffer = []
  907. last_path_aperture = None
  908. current_aperture = None
  909. # 1,2 or 3 from "G01", "G02" or "G03"
  910. current_interpolation_mode = None
  911. # 1 or 2 from "D01" or "D02"
  912. # Note this is to support deprecated Gerber not putting
  913. # an operation code at the end of every coordinate line.
  914. current_operation_code = None
  915. # Current coordinates
  916. current_x = None
  917. current_y = None
  918. # Absolute or Relative/Incremental coordinates
  919. # Not implemented
  920. absolute = True
  921. # How to interpret circular interpolation: SINGLE or MULTI
  922. quadrant_mode = None
  923. # Indicates we are parsing an aperture macro
  924. current_macro = None
  925. # Indicates the current polarity: D-Dark, C-Clear
  926. current_polarity = 'D'
  927. # If a region is being defined
  928. making_region = False
  929. #### Parsing starts here ####
  930. line_num = 0
  931. gline = ""
  932. try:
  933. for gline in glines:
  934. line_num += 1
  935. ### Cleanup
  936. gline = gline.strip(' \r\n')
  937. ### Aperture Macros
  938. # Having this at the beggining will slow things down
  939. # but macros can have complicated statements than could
  940. # be caught by other patterns.
  941. if current_macro is None: # No macro started yet
  942. match = self.am1_re.search(gline)
  943. # Start macro if match, else not an AM, carry on.
  944. if match:
  945. log.info("Starting macro. Line %d: %s" % (line_num, gline))
  946. current_macro = match.group(1)
  947. self.aperture_macros[current_macro] = ApertureMacro(name=current_macro)
  948. if match.group(2): # Append
  949. self.aperture_macros[current_macro].append(match.group(2))
  950. if match.group(3): # Finish macro
  951. #self.aperture_macros[current_macro].parse_content()
  952. current_macro = None
  953. log.info("Macro complete in 1 line.")
  954. continue
  955. else: # Continue macro
  956. log.info("Continuing macro. Line %d." % line_num)
  957. match = self.am2_re.search(gline)
  958. if match: # Finish macro
  959. log.info("End of macro. Line %d." % line_num)
  960. self.aperture_macros[current_macro].append(match.group(1))
  961. #self.aperture_macros[current_macro].parse_content()
  962. current_macro = None
  963. else: # Append
  964. self.aperture_macros[current_macro].append(gline)
  965. continue
  966. ### G01 - Linear interpolation plus flashes
  967. # Operation code (D0x) missing is deprecated... oh well I will support it.
  968. # REGEX: r'^(?:G0?(1))?(?:X(-?\d+))?(?:Y(-?\d+))?(?:D0([123]))?\*$'
  969. match = self.lin_re.search(gline)
  970. if match:
  971. # Dxx alone?
  972. # if match.group(1) is None and match.group(2) is None and match.group(3) is None:
  973. # try:
  974. # current_operation_code = int(match.group(4))
  975. # except:
  976. # pass # A line with just * will match too.
  977. # continue
  978. # NOTE: Letting it continue allows it to react to the
  979. # operation code.
  980. # Parse coordinates
  981. if match.group(2) is not None:
  982. current_x = parse_gerber_number(match.group(2), self.frac_digits)
  983. if match.group(3) is not None:
  984. current_y = parse_gerber_number(match.group(3), self.frac_digits)
  985. # Parse operation code
  986. if match.group(4) is not None:
  987. current_operation_code = int(match.group(4))
  988. # Pen down: add segment
  989. if current_operation_code == 1:
  990. path.append([current_x, current_y])
  991. last_path_aperture = current_aperture
  992. elif current_operation_code == 2:
  993. if len(path) > 1:
  994. ## --- BUFFERED ---
  995. if making_region:
  996. geo = Polygon(path)
  997. else:
  998. if last_path_aperture is None:
  999. log.warning("No aperture defined for curent path. (%d)" % line_num)
  1000. width = self.apertures[last_path_aperture]["size"] # TODO: WARNING this should fail!
  1001. #log.debug("Line %d: Setting aperture to %s before buffering." % (line_num, last_path_aperture))
  1002. if follow:
  1003. geo = LineString(path)
  1004. else:
  1005. geo = LineString(path).buffer(width/2)
  1006. poly_buffer.append(geo)
  1007. path = [[current_x, current_y]] # Start new path
  1008. # Flash
  1009. elif current_operation_code == 3:
  1010. # --- BUFFERED ---
  1011. flash = Gerber.create_flash_geometry(Point([current_x, current_y]),
  1012. self.apertures[current_aperture])
  1013. poly_buffer.append(flash)
  1014. continue
  1015. ### G02/3 - Circular interpolation
  1016. # 2-clockwise, 3-counterclockwise
  1017. match = self.circ_re.search(gline)
  1018. if match:
  1019. arcdir = [None, None, "cw", "ccw"]
  1020. mode, x, y, i, j, d = match.groups()
  1021. try:
  1022. x = parse_gerber_number(x, self.frac_digits)
  1023. except:
  1024. x = current_x
  1025. try:
  1026. y = parse_gerber_number(y, self.frac_digits)
  1027. except:
  1028. y = current_y
  1029. try:
  1030. i = parse_gerber_number(i, self.frac_digits)
  1031. except:
  1032. i = 0
  1033. try:
  1034. j = parse_gerber_number(j, self.frac_digits)
  1035. except:
  1036. j = 0
  1037. if quadrant_mode is None:
  1038. log.error("Found arc without preceding quadrant specification G74 or G75. (%d)" % line_num)
  1039. log.error(gline)
  1040. continue
  1041. if mode is None and current_interpolation_mode not in [2, 3]:
  1042. log.error("Found arc without circular interpolation mode defined. (%d)" % line_num)
  1043. log.error(gline)
  1044. continue
  1045. elif mode is not None:
  1046. current_interpolation_mode = int(mode)
  1047. # Set operation code if provided
  1048. if d is not None:
  1049. current_operation_code = int(d)
  1050. # Nothing created! Pen Up.
  1051. if current_operation_code == 2:
  1052. log.warning("Arc with D2. (%d)" % line_num)
  1053. if len(path) > 1:
  1054. if last_path_aperture is None:
  1055. log.warning("No aperture defined for curent path. (%d)" % line_num)
  1056. # --- BUFFERED ---
  1057. width = self.apertures[last_path_aperture]["size"]
  1058. buffered = LineString(path).buffer(width/2)
  1059. poly_buffer.append(buffered)
  1060. current_x = x
  1061. current_y = y
  1062. path = [[current_x, current_y]] # Start new path
  1063. continue
  1064. # Flash should not happen here
  1065. if current_operation_code == 3:
  1066. log.error("Trying to flash within arc. (%d)" % line_num)
  1067. continue
  1068. if quadrant_mode == 'MULTI':
  1069. center = [i + current_x, j + current_y]
  1070. radius = sqrt(i ** 2 + j ** 2)
  1071. start = arctan2(-j, -i) # Start angle
  1072. # Numerical errors might prevent start == stop therefore
  1073. # we check ahead of time. This should result in a
  1074. # 360 degree arc.
  1075. if current_x == x and current_y == y:
  1076. stop = start
  1077. else:
  1078. stop = arctan2(-center[1] + y, -center[0] + x) # Stop angle
  1079. this_arc = arc(center, radius, start, stop,
  1080. arcdir[current_interpolation_mode],
  1081. self.steps_per_circ)
  1082. # Last point in path is current point
  1083. current_x = this_arc[-1][0]
  1084. current_y = this_arc[-1][1]
  1085. # Append
  1086. path += this_arc
  1087. last_path_aperture = current_aperture
  1088. continue
  1089. if quadrant_mode == 'SINGLE':
  1090. center_candidates = [
  1091. [i + current_x, j + current_y],
  1092. [-i + current_x, j + current_y],
  1093. [i + current_x, -j + current_y],
  1094. [-i + current_x, -j + current_y]
  1095. ]
  1096. valid = False
  1097. log.debug("I: %f J: %f" % (i, j))
  1098. for center in center_candidates:
  1099. radius = sqrt(i**2 + j**2)
  1100. # Make sure radius to start is the same as radius to end.
  1101. radius2 = sqrt((center[0] - x)**2 + (center[1] - y)**2)
  1102. if radius2 < radius*0.95 or radius2 > radius*1.05:
  1103. continue # Not a valid center.
  1104. # Correct i and j and continue as with multi-quadrant.
  1105. i = center[0] - current_x
  1106. j = center[1] - current_y
  1107. start = arctan2(-j, -i) # Start angle
  1108. stop = arctan2(-center[1] + y, -center[0] + x) # Stop angle
  1109. angle = abs(arc_angle(start, stop, arcdir[current_interpolation_mode]))
  1110. log.debug("ARC START: %f, %f CENTER: %f, %f STOP: %f, %f" %
  1111. (current_x, current_y, center[0], center[1], x, y))
  1112. log.debug("START Ang: %f, STOP Ang: %f, DIR: %s, ABS: %.12f <= %.12f: %s" %
  1113. (start*180/pi, stop*180/pi, arcdir[current_interpolation_mode],
  1114. angle*180/pi, pi/2*180/pi, angle <= (pi+1e-6)/2))
  1115. if angle <= (pi+1e-6)/2:
  1116. log.debug("########## ACCEPTING ARC ############")
  1117. this_arc = arc(center, radius, start, stop,
  1118. arcdir[current_interpolation_mode],
  1119. self.steps_per_circ)
  1120. current_x = this_arc[-1][0]
  1121. current_y = this_arc[-1][1]
  1122. path += this_arc
  1123. last_path_aperture = current_aperture
  1124. valid = True
  1125. break
  1126. if valid:
  1127. continue
  1128. else:
  1129. log.warning("Invalid arc in line %d." % line_num)
  1130. ### Operation code alone
  1131. # Operation code alone, usually just D03 (Flash)
  1132. # self.opcode_re = re.compile(r'^D0?([123])\*$')
  1133. match = self.opcode_re.search(gline)
  1134. if match:
  1135. current_operation_code = int(match.group(1))
  1136. if current_operation_code == 3:
  1137. ## --- Buffered ---
  1138. try:
  1139. flash = Gerber.create_flash_geometry(Point(path[-1]),
  1140. self.apertures[current_aperture])
  1141. poly_buffer.append(flash)
  1142. except IndexError:
  1143. log.warning("Line %d: %s -> Nothing there to flash!" % (line_num, gline))
  1144. continue
  1145. ### G74/75* - Single or multiple quadrant arcs
  1146. match = self.quad_re.search(gline)
  1147. if match:
  1148. if match.group(1) == '4':
  1149. quadrant_mode = 'SINGLE'
  1150. else:
  1151. quadrant_mode = 'MULTI'
  1152. continue
  1153. ### G36* - Begin region
  1154. if self.regionon_re.search(gline):
  1155. if len(path) > 1:
  1156. # Take care of what is left in the path
  1157. ## --- Buffered ---
  1158. width = self.apertures[last_path_aperture]["size"]
  1159. geo = LineString(path).buffer(width/2)
  1160. poly_buffer.append(geo)
  1161. path = [path[-1]]
  1162. making_region = True
  1163. continue
  1164. ### G37* - End region
  1165. if self.regionoff_re.search(gline):
  1166. making_region = False
  1167. # Only one path defines region?
  1168. # This can happen if D02 happened before G37 and
  1169. # is not and error.
  1170. if len(path) < 3:
  1171. # print "ERROR: Path contains less than 3 points:"
  1172. # print path
  1173. # print "Line (%d): " % line_num, gline
  1174. # path = []
  1175. #path = [[current_x, current_y]]
  1176. continue
  1177. # For regions we may ignore an aperture that is None
  1178. # self.regions.append({"polygon": Polygon(path),
  1179. # "aperture": last_path_aperture})
  1180. # --- Buffered ---
  1181. region = Polygon(path)
  1182. if not region.is_valid:
  1183. region = region.buffer(0)
  1184. poly_buffer.append(region)
  1185. path = [[current_x, current_y]] # Start new path
  1186. continue
  1187. ### Aperture definitions %ADD...
  1188. match = self.ad_re.search(gline)
  1189. if match:
  1190. log.info("Found aperture definition. Line %d: %s" % (line_num, gline))
  1191. self.aperture_parse(match.group(1), match.group(2), match.group(3))
  1192. continue
  1193. ### G01/2/3* - Interpolation mode change
  1194. # Can occur along with coordinates and operation code but
  1195. # sometimes by itself (handled here).
  1196. # Example: G01*
  1197. match = self.interp_re.search(gline)
  1198. if match:
  1199. current_interpolation_mode = int(match.group(1))
  1200. continue
  1201. ### Tool/aperture change
  1202. # Example: D12*
  1203. match = self.tool_re.search(gline)
  1204. if match:
  1205. log.debug("Line %d: Aperture change to (%s)" % (line_num, match.group(1)))
  1206. current_aperture = match.group(1)
  1207. # Take care of the current path with the previous tool
  1208. if len(path) > 1:
  1209. # --- Buffered ----
  1210. width = self.apertures[last_path_aperture]["size"]
  1211. geo = LineString(path).buffer(width/2)
  1212. poly_buffer.append(geo)
  1213. path = [path[-1]]
  1214. continue
  1215. ### Polarity change
  1216. # Example: %LPD*% or %LPC*%
  1217. # If polarity changes, creates geometry from current
  1218. # buffer, then adds or subtracts accordingly.
  1219. match = self.lpol_re.search(gline)
  1220. if match:
  1221. if len(path) > 1 and current_polarity != match.group(1):
  1222. # --- Buffered ----
  1223. width = self.apertures[last_path_aperture]["size"]
  1224. geo = LineString(path).buffer(width / 2)
  1225. poly_buffer.append(geo)
  1226. path = [path[-1]]
  1227. # --- Apply buffer ---
  1228. # If added for testing of bug #83
  1229. # TODO: Remove when bug fixed
  1230. if len(poly_buffer) > 0:
  1231. if current_polarity == 'D':
  1232. self.solid_geometry = self.solid_geometry.union(cascaded_union(poly_buffer))
  1233. else:
  1234. self.solid_geometry = self.solid_geometry.difference(cascaded_union(poly_buffer))
  1235. poly_buffer = []
  1236. current_polarity = match.group(1)
  1237. continue
  1238. ### Number format
  1239. # Example: %FSLAX24Y24*%
  1240. # TODO: This is ignoring most of the format. Implement the rest.
  1241. match = self.fmt_re.search(gline)
  1242. if match:
  1243. absolute = {'A': True, 'I': False}
  1244. self.int_digits = int(match.group(3))
  1245. self.frac_digits = int(match.group(4))
  1246. continue
  1247. ### Mode (IN/MM)
  1248. # Example: %MOIN*%
  1249. match = self.mode_re.search(gline)
  1250. if match:
  1251. self.units = match.group(1)
  1252. continue
  1253. ### Units (G70/1) OBSOLETE
  1254. match = self.units_re.search(gline)
  1255. if match:
  1256. self.units = {'0': 'IN', '1': 'MM'}[match.group(1)]
  1257. continue
  1258. ### Absolute/relative coordinates G90/1 OBSOLETE
  1259. match = self.absrel_re.search(gline)
  1260. if match:
  1261. absolute = {'0': True, '1': False}[match.group(1)]
  1262. continue
  1263. #### Ignored lines
  1264. ## Comments
  1265. match = self.comm_re.search(gline)
  1266. if match:
  1267. continue
  1268. ## EOF
  1269. match = self.eof_re.search(gline)
  1270. if match:
  1271. continue
  1272. ### Line did not match any pattern. Warn user.
  1273. log.warning("Line ignored (%d): %s" % (line_num, gline))
  1274. if len(path) > 1:
  1275. # EOF, create shapely LineString if something still in path
  1276. ## --- Buffered ---
  1277. width = self.apertures[last_path_aperture]["size"]
  1278. geo = LineString(path).buffer(width/2)
  1279. poly_buffer.append(geo)
  1280. # --- Apply buffer ---
  1281. if current_polarity == 'D':
  1282. self.solid_geometry = self.solid_geometry.union(cascaded_union(poly_buffer))
  1283. else:
  1284. self.solid_geometry = self.solid_geometry.difference(cascaded_union(poly_buffer))
  1285. except Exception, err:
  1286. #print traceback.format_exc()
  1287. log.error("PARSING FAILED. Line %d: %s" % (line_num, gline))
  1288. raise
  1289. @staticmethod
  1290. def create_flash_geometry(location, aperture):
  1291. if type(location) == list:
  1292. location = Point(location)
  1293. if aperture['type'] == 'C': # Circles
  1294. return location.buffer(aperture['size']/2)
  1295. if aperture['type'] == 'R': # Rectangles
  1296. loc = location.coords[0]
  1297. width = aperture['width']
  1298. height = aperture['height']
  1299. minx = loc[0] - width / 2
  1300. maxx = loc[0] + width / 2
  1301. miny = loc[1] - height / 2
  1302. maxy = loc[1] + height / 2
  1303. return shply_box(minx, miny, maxx, maxy)
  1304. if aperture['type'] == 'O': # Obround
  1305. loc = location.coords[0]
  1306. width = aperture['width']
  1307. height = aperture['height']
  1308. if width > height:
  1309. p1 = Point(loc[0] + 0.5 * (width - height), loc[1])
  1310. p2 = Point(loc[0] - 0.5 * (width - height), loc[1])
  1311. c1 = p1.buffer(height * 0.5)
  1312. c2 = p2.buffer(height * 0.5)
  1313. else:
  1314. p1 = Point(loc[0], loc[1] + 0.5 * (height - width))
  1315. p2 = Point(loc[0], loc[1] - 0.5 * (height - width))
  1316. c1 = p1.buffer(width * 0.5)
  1317. c2 = p2.buffer(width * 0.5)
  1318. return cascaded_union([c1, c2]).convex_hull
  1319. if aperture['type'] == 'P': # Regular polygon
  1320. loc = location.coords[0]
  1321. diam = aperture['diam']
  1322. n_vertices = aperture['nVertices']
  1323. points = []
  1324. for i in range(0, n_vertices):
  1325. x = loc[0] + diam * (cos(2 * pi * i / n_vertices))
  1326. y = loc[1] + diam * (sin(2 * pi * i / n_vertices))
  1327. points.append((x, y))
  1328. ply = Polygon(points)
  1329. if 'rotation' in aperture:
  1330. ply = affinity.rotate(ply, aperture['rotation'])
  1331. return ply
  1332. if aperture['type'] == 'AM': # Aperture Macro
  1333. loc = location.coords[0]
  1334. flash_geo = aperture['macro'].make_geometry(aperture['modifiers'])
  1335. return affinity.translate(flash_geo, xoff=loc[0], yoff=loc[1])
  1336. return None
  1337. def create_geometry(self):
  1338. """
  1339. Geometry from a Gerber file is made up entirely of polygons.
  1340. Every stroke (linear or circular) has an aperture which gives
  1341. it thickness. Additionally, aperture strokes have non-zero area,
  1342. and regions naturally do as well.
  1343. :rtype : None
  1344. :return: None
  1345. """
  1346. # self.buffer_paths()
  1347. #
  1348. # self.fix_regions()
  1349. #
  1350. # self.do_flashes()
  1351. #
  1352. # self.solid_geometry = cascaded_union(self.buffered_paths +
  1353. # [poly['polygon'] for poly in self.regions] +
  1354. # self.flash_geometry)
  1355. def get_bounding_box(self, margin=0.0, rounded=False):
  1356. """
  1357. Creates and returns a rectangular polygon bounding at a distance of
  1358. margin from the object's ``solid_geometry``. If margin > 0, the polygon
  1359. can optionally have rounded corners of radius equal to margin.
  1360. :param margin: Distance to enlarge the rectangular bounding
  1361. box in both positive and negative, x and y axes.
  1362. :type margin: float
  1363. :param rounded: Wether or not to have rounded corners.
  1364. :type rounded: bool
  1365. :return: The bounding box.
  1366. :rtype: Shapely.Polygon
  1367. """
  1368. bbox = self.solid_geometry.envelope.buffer(margin)
  1369. if not rounded:
  1370. bbox = bbox.envelope
  1371. return bbox
  1372. class Excellon(Geometry):
  1373. """
  1374. *ATTRIBUTES*
  1375. * ``tools`` (dict): The key is the tool name and the value is
  1376. a dictionary specifying the tool:
  1377. ================ ====================================
  1378. Key Value
  1379. ================ ====================================
  1380. C Diameter of the tool
  1381. Others Not supported (Ignored).
  1382. ================ ====================================
  1383. * ``drills`` (list): Each is a dictionary:
  1384. ================ ====================================
  1385. Key Value
  1386. ================ ====================================
  1387. point (Shapely.Point) Where to drill
  1388. tool (str) A key in ``tools``
  1389. ================ ====================================
  1390. """
  1391. def __init__(self, zeros="L"):
  1392. """
  1393. The constructor takes no parameters.
  1394. :return: Excellon object.
  1395. :rtype: Excellon
  1396. """
  1397. Geometry.__init__(self)
  1398. self.tools = {}
  1399. self.drills = []
  1400. # Trailing "T" or leading "L" (default)
  1401. #self.zeros = "T"
  1402. self.zeros = zeros
  1403. # Attributes to be included in serialization
  1404. # Always append to it because it carries contents
  1405. # from Geometry.
  1406. self.ser_attrs += ['tools', 'drills', 'zeros']
  1407. #### Patterns ####
  1408. # Regex basics:
  1409. # ^ - beginning
  1410. # $ - end
  1411. # *: 0 or more, +: 1 or more, ?: 0 or 1
  1412. # M48 - Beggining of Part Program Header
  1413. self.hbegin_re = re.compile(r'^M48$')
  1414. # M95 or % - End of Part Program Header
  1415. # NOTE: % has different meaning in the body
  1416. self.hend_re = re.compile(r'^(?:M95|%)$')
  1417. # FMAT Excellon format
  1418. self.fmat_re = re.compile(r'^FMAT,([12])$')
  1419. # Number format and units
  1420. # INCH uses 6 digits
  1421. # METRIC uses 5/6
  1422. self.units_re = re.compile(r'^(INCH|METRIC)(?:,([TL])Z)?$')
  1423. # Tool definition/parameters (?= is look-ahead
  1424. # NOTE: This might be an overkill!
  1425. # self.toolset_re = re.compile(r'^T(0?\d|\d\d)(?=.*C(\d*\.?\d*))?' +
  1426. # r'(?=.*F(\d*\.?\d*))?(?=.*S(\d*\.?\d*))?' +
  1427. # r'(?=.*B(\d*\.?\d*))?(?=.*H(\d*\.?\d*))?' +
  1428. # r'(?=.*Z([-\+]?\d*\.?\d*))?[CFSBHT]')
  1429. self.toolset_re = re.compile(r'^T(\d+)(?=.*C(\d*\.?\d*))?' +
  1430. r'(?=.*F(\d*\.?\d*))?(?=.*S(\d*\.?\d*))?' +
  1431. r'(?=.*B(\d*\.?\d*))?(?=.*H(\d*\.?\d*))?' +
  1432. r'(?=.*Z([-\+]?\d*\.?\d*))?[CFSBHT]')
  1433. # Tool select
  1434. # Can have additional data after tool number but
  1435. # is ignored if present in the header.
  1436. # Warning: This will match toolset_re too.
  1437. # self.toolsel_re = re.compile(r'^T((?:\d\d)|(?:\d))')
  1438. self.toolsel_re = re.compile(r'^T(\d+)')
  1439. # Comment
  1440. self.comm_re = re.compile(r'^;(.*)$')
  1441. # Absolute/Incremental G90/G91
  1442. self.absinc_re = re.compile(r'^G9([01])$')
  1443. # Modes of operation
  1444. # 1-linear, 2-circCW, 3-cirCCW, 4-vardwell, 5-Drill
  1445. self.modes_re = re.compile(r'^G0([012345])')
  1446. # Measuring mode
  1447. # 1-metric, 2-inch
  1448. self.meas_re = re.compile(r'^M7([12])$')
  1449. # Coordinates
  1450. #self.xcoord_re = re.compile(r'^X(\d*\.?\d*)(?:Y\d*\.?\d*)?$')
  1451. #self.ycoord_re = re.compile(r'^(?:X\d*\.?\d*)?Y(\d*\.?\d*)$')
  1452. self.coordsperiod_re = re.compile(r'(?=.*X([-\+]?\d*\.\d*))?(?=.*Y([-\+]?\d*\.\d*))?[XY]')
  1453. self.coordsnoperiod_re = re.compile(r'(?!.*\.)(?=.*X([-\+]?\d*))?(?=.*Y([-\+]?\d*))?[XY]')
  1454. # R - Repeat hole (# times, X offset, Y offset)
  1455. self.rep_re = re.compile(r'^R(\d+)(?=.*[XY])+(?:X([-\+]?\d*\.?\d*))?(?:Y([-\+]?\d*\.?\d*))?$')
  1456. # Various stop/pause commands
  1457. self.stop_re = re.compile(r'^((G04)|(M09)|(M06)|(M00)|(M30))')
  1458. # Parse coordinates
  1459. self.leadingzeros_re = re.compile(r'^[-\+]?(0*)(\d*)')
  1460. def parse_file(self, filename):
  1461. """
  1462. Reads the specified file as array of lines as
  1463. passes it to ``parse_lines()``.
  1464. :param filename: The file to be read and parsed.
  1465. :type filename: str
  1466. :return: None
  1467. """
  1468. efile = open(filename, 'r')
  1469. estr = efile.readlines()
  1470. efile.close()
  1471. self.parse_lines(estr)
  1472. def parse_lines(self, elines):
  1473. """
  1474. Main Excellon parser.
  1475. :param elines: List of strings, each being a line of Excellon code.
  1476. :type elines: list
  1477. :return: None
  1478. """
  1479. # State variables
  1480. current_tool = ""
  1481. in_header = False
  1482. current_x = None
  1483. current_y = None
  1484. #### Parsing starts here ####
  1485. line_num = 0 # Line number
  1486. for eline in elines:
  1487. line_num += 1
  1488. ### Cleanup lines
  1489. eline = eline.strip(' \r\n')
  1490. ## Header Begin/End ##
  1491. if self.hbegin_re.search(eline):
  1492. in_header = True
  1493. continue
  1494. if self.hend_re.search(eline):
  1495. in_header = False
  1496. continue
  1497. #### Body ####
  1498. if not in_header:
  1499. ## Tool change ##
  1500. match = self.toolsel_re.search(eline)
  1501. if match:
  1502. current_tool = str(int(match.group(1)))
  1503. continue
  1504. ## Coordinates without period ##
  1505. match = self.coordsnoperiod_re.search(eline)
  1506. if match:
  1507. try:
  1508. #x = float(match.group(1))/10000
  1509. x = self.parse_number(match.group(1))
  1510. current_x = x
  1511. except TypeError:
  1512. x = current_x
  1513. try:
  1514. #y = float(match.group(2))/10000
  1515. y = self.parse_number(match.group(2))
  1516. current_y = y
  1517. except TypeError:
  1518. y = current_y
  1519. if x is None or y is None:
  1520. log.error("Missing coordinates")
  1521. continue
  1522. self.drills.append({'point': Point((x, y)), 'tool': current_tool})
  1523. continue
  1524. ## Coordinates with period: Use literally. ##
  1525. match = self.coordsperiod_re.search(eline)
  1526. if match:
  1527. try:
  1528. x = float(match.group(1))
  1529. current_x = x
  1530. except TypeError:
  1531. x = current_x
  1532. try:
  1533. y = float(match.group(2))
  1534. current_y = y
  1535. except TypeError:
  1536. y = current_y
  1537. if x is None or y is None:
  1538. log.error("Missing coordinates")
  1539. continue
  1540. self.drills.append({'point': Point((x, y)), 'tool': current_tool})
  1541. continue
  1542. #### Header ####
  1543. if in_header:
  1544. ## Tool definitions ##
  1545. match = self.toolset_re.search(eline)
  1546. if match:
  1547. name = str(int(match.group(1)))
  1548. spec = {
  1549. "C": float(match.group(2)),
  1550. # "F": float(match.group(3)),
  1551. # "S": float(match.group(4)),
  1552. # "B": float(match.group(5)),
  1553. # "H": float(match.group(6)),
  1554. # "Z": float(match.group(7))
  1555. }
  1556. self.tools[name] = spec
  1557. continue
  1558. ## Units and number format ##
  1559. match = self.units_re.match(eline)
  1560. if match:
  1561. self.zeros = match.group(2) or self.zeros # "T" or "L". Might be empty
  1562. self.units = {"INCH": "IN", "METRIC": "MM"}[match.group(1)]
  1563. continue
  1564. log.warning("Line ignored: %s" % eline)
  1565. log.info("Zeros: %s, Units %s." % (self.zeros, self.units))
  1566. def parse_number(self, number_str):
  1567. """
  1568. Parses coordinate numbers without period.
  1569. :param number_str: String representing the numerical value.
  1570. :type number_str: str
  1571. :return: Floating point representation of the number
  1572. :rtype: foat
  1573. """
  1574. if self.zeros == "L":
  1575. # With leading zeros, when you type in a coordinate,
  1576. # the leading zeros must always be included. Trailing zeros
  1577. # are unneeded and may be left off. The CNC-7 will automatically add them.
  1578. # r'^[-\+]?(0*)(\d*)'
  1579. # 6 digits are divided by 10^4
  1580. # If less than size digits, they are automatically added,
  1581. # 5 digits then are divided by 10^3 and so on.
  1582. match = self.leadingzeros_re.search(number_str)
  1583. return float(number_str)/(10**(len(match.group(1)) + len(match.group(2)) - 2))
  1584. else: # Trailing
  1585. # You must show all zeros to the right of the number and can omit
  1586. # all zeros to the left of the number. The CNC-7 will count the number
  1587. # of digits you typed and automatically fill in the missing zeros.
  1588. if self.units.lower() == "in": # Inches is 00.0000
  1589. return float(number_str)/10000
  1590. return float(number_str)/1000 # Metric is 000.000
  1591. def create_geometry(self):
  1592. """
  1593. Creates circles of the tool diameter at every point
  1594. specified in ``self.drills``.
  1595. :return: None
  1596. """
  1597. self.solid_geometry = []
  1598. for drill in self.drills:
  1599. #poly = drill['point'].buffer(self.tools[drill['tool']]["C"]/2.0)
  1600. tooldia = self.tools[drill['tool']]['C']
  1601. poly = drill['point'].buffer(tooldia/2.0)
  1602. self.solid_geometry.append(poly)
  1603. def scale(self, factor):
  1604. """
  1605. Scales geometry on the XY plane in the object by a given factor.
  1606. Tool sizes, feedrates an Z-plane dimensions are untouched.
  1607. :param factor: Number by which to scale the object.
  1608. :type factor: float
  1609. :return: None
  1610. :rtype: NOne
  1611. """
  1612. # Drills
  1613. for drill in self.drills:
  1614. drill['point'] = affinity.scale(drill['point'], factor, factor, origin=(0, 0))
  1615. self.create_geometry()
  1616. def offset(self, vect):
  1617. """
  1618. Offsets geometry on the XY plane in the object by a given vector.
  1619. :param vect: (x, y) offset vector.
  1620. :type vect: tuple
  1621. :return: None
  1622. """
  1623. dx, dy = vect
  1624. # Drills
  1625. for drill in self.drills:
  1626. drill['point'] = affinity.translate(drill['point'], xoff=dx, yoff=dy)
  1627. # Recreate geometry
  1628. self.create_geometry()
  1629. def mirror(self, axis, point):
  1630. """
  1631. :param axis: "X" or "Y" indicates around which axis to mirror.
  1632. :type axis: str
  1633. :param point: [x, y] point belonging to the mirror axis.
  1634. :type point: list
  1635. :return: None
  1636. """
  1637. px, py = point
  1638. xscale, yscale = {"X": (1.0, -1.0), "Y": (-1.0, 1.0)}[axis]
  1639. # Modify data
  1640. for drill in self.drills:
  1641. drill['point'] = affinity.scale(drill['point'], xscale, yscale, origin=(px, py))
  1642. # Recreate geometry
  1643. self.create_geometry()
  1644. def convert_units(self, units):
  1645. factor = Geometry.convert_units(self, units)
  1646. # Tools
  1647. for tname in self.tools:
  1648. self.tools[tname]["C"] *= factor
  1649. self.create_geometry()
  1650. return factor
  1651. class CNCjob(Geometry):
  1652. """
  1653. Represents work to be done by a CNC machine.
  1654. *ATTRIBUTES*
  1655. * ``gcode_parsed`` (list): Each is a dictionary:
  1656. ===================== =========================================
  1657. Key Value
  1658. ===================== =========================================
  1659. geom (Shapely.LineString) Tool path (XY plane)
  1660. kind (string) "AB", A is "T" (travel) or
  1661. "C" (cut). B is "F" (fast) or "S" (slow).
  1662. ===================== =========================================
  1663. """
  1664. defaults = {
  1665. "zdownrate": None
  1666. }
  1667. def __init__(self, units="in", kind="generic", z_move=0.1,
  1668. feedrate=3.0, z_cut=-0.002, tooldia=0.0, zdownrate=None):
  1669. Geometry.__init__(self)
  1670. self.kind = kind
  1671. self.units = units
  1672. self.z_cut = z_cut
  1673. self.z_move = z_move
  1674. self.feedrate = feedrate
  1675. self.tooldia = tooldia
  1676. self.unitcode = {"IN": "G20", "MM": "G21"}
  1677. self.pausecode = "G04 P1"
  1678. self.feedminutecode = "G94"
  1679. self.absolutecode = "G90"
  1680. self.gcode = ""
  1681. self.input_geometry_bounds = None
  1682. self.gcode_parsed = None
  1683. self.steps_per_circ = 20 # Used when parsing G-code arcs
  1684. if zdownrate is not None:
  1685. self.zdownrate = float(zdownrate)
  1686. elif CNCjob.defaults["zdownrate"] is not None:
  1687. self.zdownrate = float(CNCjob.defaults["zdownrate"])
  1688. else:
  1689. self.zdownrate = None
  1690. # Attributes to be included in serialization
  1691. # Always append to it because it carries contents
  1692. # from Geometry.
  1693. self.ser_attrs += ['kind', 'z_cut', 'z_move', 'feedrate', 'tooldia',
  1694. 'gcode', 'input_geometry_bounds', 'gcode_parsed',
  1695. 'steps_per_circ']
  1696. # Buffer for linear (No polygons or iterable geometry) elements
  1697. # and their properties.
  1698. self.flat_geometry = []
  1699. # 2D index of self.flat_geometry
  1700. self.flat_geometry_rtree = rtindex.Index()
  1701. # Current insert position to flat_geometry
  1702. self.fg_current_index = 0
  1703. def flatten(self, geo):
  1704. """
  1705. Flattens the input geometry into an array of non-iterable geometry
  1706. elements and indexes into rtree by their first and last coordinate
  1707. pairs.
  1708. :param geo:
  1709. :return:
  1710. """
  1711. try:
  1712. for g in geo:
  1713. self.flatten(g)
  1714. except TypeError: # is not iterable
  1715. self.flat_geometry.append({"path": geo})
  1716. self.flat_geometry_rtree.insert(self.fg_current_index, geo.coords[0])
  1717. self.flat_geometry_rtree.insert(self.fg_current_index, geo.coords[-1])
  1718. self.fg_current_index += 1
  1719. def convert_units(self, units):
  1720. factor = Geometry.convert_units(self, units)
  1721. log.debug("CNCjob.convert_units()")
  1722. self.z_cut *= factor
  1723. self.z_move *= factor
  1724. self.feedrate *= factor
  1725. self.tooldia *= factor
  1726. return factor
  1727. def generate_from_excellon(self, exobj):
  1728. """
  1729. Generates G-code for drilling from Excellon object.
  1730. self.gcode becomes a list, each element is a
  1731. different job for each tool in the excellon code.
  1732. """
  1733. self.kind = "drill"
  1734. self.gcode = []
  1735. t = "G00 X%.4fY%.4f\n"
  1736. down = "G01 Z%.4f\n" % self.z_cut
  1737. up = "G01 Z%.4f\n" % self.z_move
  1738. for tool in exobj.tools:
  1739. points = []
  1740. for drill in exobj.drill:
  1741. if drill['tool'] == tool:
  1742. points.append(drill['point'])
  1743. gcode = self.unitcode[self.units.upper()] + "\n"
  1744. gcode += self.absolutecode + "\n"
  1745. gcode += self.feedminutecode + "\n"
  1746. gcode += "F%.2f\n" % self.feedrate
  1747. gcode += "G00 Z%.4f\n" % self.z_move # Move to travel height
  1748. gcode += "M03\n" # Spindle start
  1749. gcode += self.pausecode + "\n"
  1750. for point in points:
  1751. gcode += t % point
  1752. gcode += down + up
  1753. gcode += t % (0, 0)
  1754. gcode += "M05\n" # Spindle stop
  1755. self.gcode.append(gcode)
  1756. def generate_from_excellon_by_tool(self, exobj, tools="all"):
  1757. """
  1758. Creates gcode for this object from an Excellon object
  1759. for the specified tools.
  1760. :param exobj: Excellon object to process
  1761. :type exobj: Excellon
  1762. :param tools: Comma separated tool names
  1763. :type: tools: str
  1764. :return: None
  1765. :rtype: None
  1766. """
  1767. log.debug("Creating CNC Job from Excellon...")
  1768. if tools == "all":
  1769. tools = [tool for tool in exobj.tools]
  1770. else:
  1771. tools = [x.strip() for x in tools.split(",")]
  1772. tools = filter(lambda i: i in exobj.tools, tools)
  1773. log.debug("Tools are: %s" % str(tools))
  1774. points = []
  1775. for drill in exobj.drills:
  1776. if drill['tool'] in tools:
  1777. points.append(drill['point'])
  1778. log.debug("Found %d drills." % len(points))
  1779. #self.kind = "drill"
  1780. self.gcode = []
  1781. t = "G00 X%.4fY%.4f\n"
  1782. down = "G01 Z%.4f\n" % self.z_cut
  1783. up = "G01 Z%.4f\n" % self.z_move
  1784. gcode = self.unitcode[self.units.upper()] + "\n"
  1785. gcode += self.absolutecode + "\n"
  1786. gcode += self.feedminutecode + "\n"
  1787. gcode += "F%.2f\n" % self.feedrate
  1788. gcode += "G00 Z%.4f\n" % self.z_move # Move to travel height
  1789. gcode += "M03\n" # Spindle start
  1790. gcode += self.pausecode + "\n"
  1791. for point in points:
  1792. x, y = point.coords.xy
  1793. gcode += t % (x[0], y[0])
  1794. gcode += down + up
  1795. gcode += t % (0, 0)
  1796. gcode += "M05\n" # Spindle stop
  1797. self.gcode = gcode
  1798. def generate_from_geometry(self, geometry, append=True, tooldia=None, tolerance=0):
  1799. """
  1800. Generates G-Code from a Geometry object. Stores in ``self.gcode``.
  1801. Algorithm description:
  1802. ----------------------
  1803. Follow geometry paths in the order they are being read. No attempt
  1804. to optimize.
  1805. :param geometry: Geometry defining the toolpath
  1806. :type geometry: Geometry
  1807. :param append: Wether to append to self.gcode or re-write it.
  1808. :type append: bool
  1809. :param tooldia: If given, sets the tooldia property but does
  1810. not affect the process in any other way.
  1811. :type tooldia: bool
  1812. :param tolerance: All points in the simplified object will be within the
  1813. tolerance distance of the original geometry.
  1814. :return: None
  1815. :rtype: None
  1816. """
  1817. if tooldia is not None:
  1818. self.tooldia = tooldia
  1819. self.input_geometry_bounds = geometry.bounds()
  1820. if not append:
  1821. self.gcode = ""
  1822. # Initial G-Code
  1823. self.gcode = self.unitcode[self.units.upper()] + "\n"
  1824. self.gcode += self.absolutecode + "\n"
  1825. self.gcode += self.feedminutecode + "\n"
  1826. self.gcode += "F%.2f\n" % self.feedrate
  1827. self.gcode += "G00 Z%.4f\n" % self.z_move # Move (up) to travel height
  1828. self.gcode += "M03\n" # Spindle start
  1829. self.gcode += self.pausecode + "\n"
  1830. # Iterate over geometry and run individual methods
  1831. # depending on type
  1832. for geo in geometry.solid_geometry:
  1833. if type(geo) == Polygon:
  1834. self.gcode += self.polygon2gcode(geo, tolerance=tolerance)
  1835. continue
  1836. if type(geo) == LineString or type(geo) == LinearRing:
  1837. self.gcode += self.linear2gcode(geo, tolerance=tolerance)
  1838. continue
  1839. if type(geo) == Point:
  1840. self.gcode += self.point2gcode(geo)
  1841. continue
  1842. if type(geo) == MultiPolygon:
  1843. for poly in geo:
  1844. self.gcode += self.polygon2gcode(poly, tolerance=tolerance)
  1845. continue
  1846. log.warning("G-code generation not implemented for %s" % (str(type(geo))))
  1847. # Finish
  1848. self.gcode += "G00 Z%.4f\n" % self.z_move # Stop cutting
  1849. self.gcode += "G00 X0Y0\n"
  1850. self.gcode += "M05\n" # Spindle stop
  1851. def generate_from_geometry_2(self, geometry, append=True, tooldia=None, tolerance=0):
  1852. """
  1853. Second algorithm to generate from Geometry.
  1854. ALgorithm description:
  1855. ----------------------
  1856. Uses RTree to find the nearest path to follow.
  1857. :param geometry:
  1858. :param append:
  1859. :param tooldia:
  1860. :param tolerance:
  1861. :return: None
  1862. """
  1863. assert isinstance(geometry, Geometry)
  1864. ## Flatten the geometry and get rtree index
  1865. flat_geometry, rti = geometry.flatten_to_paths()
  1866. log.debug("%d paths" % len(flat_geometry))
  1867. if tooldia is not None:
  1868. self.tooldia = tooldia
  1869. self.input_geometry_bounds = geometry.bounds()
  1870. if not append:
  1871. self.gcode = ""
  1872. # Initial G-Code
  1873. self.gcode = self.unitcode[self.units.upper()] + "\n"
  1874. self.gcode += self.absolutecode + "\n"
  1875. self.gcode += self.feedminutecode + "\n"
  1876. self.gcode += "F%.2f\n" % self.feedrate
  1877. self.gcode += "G00 Z%.4f\n" % self.z_move # Move (up) to travel height
  1878. self.gcode += "M03\n" # Spindle start
  1879. self.gcode += self.pausecode + "\n"
  1880. ## Iterate over geometry paths getting the nearest each time.
  1881. path_count = 0
  1882. current_pt = (0, 0)
  1883. hits = list(rti.nearest(current_pt, 1))
  1884. while len(hits) > 0:
  1885. path_count += 1
  1886. print "Current: ", "(%.3f, %.3f)" % current_pt
  1887. geo = flat_geometry[hits[0]]
  1888. # Determine which end of the path is closest.
  1889. distance2start = distance(current_pt, geo.coords[0])
  1890. distance2stop = distance(current_pt, geo.coords[-1])
  1891. print " Path index =", hits[0]
  1892. print " Start: ", "(%.3f, %.3f)" % geo.coords[0], " D(Start): %.3f" % distance2start
  1893. print " Stop : ", "(%.3f, %.3f)" % geo.coords[-1], " D(Stop): %.3f" % distance2stop
  1894. # Reverse if end is closest.
  1895. if distance2start > distance2stop:
  1896. print " Reversing!"
  1897. geo.coords = list(geo.coords)[::-1]
  1898. # G-code
  1899. if type(geo) == LineString or type(geo) == LinearRing:
  1900. self.gcode += self.linear2gcode(geo, tolerance=tolerance)
  1901. elif type(geo) == Point:
  1902. self.gcode += self.point2gcode(geo)
  1903. else:
  1904. log.warning("G-code generation not implemented for %s" % (str(type(geo))))
  1905. # Delete from index, update current location and continue.
  1906. rti.delete(hits[0], geo.coords[0])
  1907. rti.delete(hits[0], geo.coords[-1])
  1908. current_pt = geo.coords[-1]
  1909. hits = list(rti.nearest(current_pt, 1))
  1910. log.debug("%s paths traced." % path_count)
  1911. # Finish
  1912. self.gcode += "G00 Z%.4f\n" % self.z_move # Stop cutting
  1913. self.gcode += "G00 X0Y0\n"
  1914. self.gcode += "M05\n" # Spindle stop
  1915. def pre_parse(self, gtext):
  1916. """
  1917. Separates parts of the G-Code text into a list of dictionaries.
  1918. Used by ``self.gcode_parse()``.
  1919. :param gtext: A single string with g-code
  1920. """
  1921. # Units: G20-inches, G21-mm
  1922. units_re = re.compile(r'^G2([01])')
  1923. # TODO: This has to be re-done
  1924. gcmds = []
  1925. lines = gtext.split("\n") # TODO: This is probably a lot of work!
  1926. for line in lines:
  1927. # Clean up
  1928. line = line.strip()
  1929. # Remove comments
  1930. # NOTE: Limited to 1 bracket pair
  1931. op = line.find("(")
  1932. cl = line.find(")")
  1933. #if op > -1 and cl > op:
  1934. if cl > op > -1:
  1935. #comment = line[op+1:cl]
  1936. line = line[:op] + line[(cl+1):]
  1937. # Units
  1938. match = units_re.match(line)
  1939. if match:
  1940. self.units = {'0': "IN", '1': "MM"}[match.group(1)]
  1941. # Parse GCode
  1942. # 0 4 12
  1943. # G01 X-0.007 Y-0.057
  1944. # --> codes_idx = [0, 4, 12]
  1945. codes = "NMGXYZIJFP"
  1946. codes_idx = []
  1947. i = 0
  1948. for ch in line:
  1949. if ch in codes:
  1950. codes_idx.append(i)
  1951. i += 1
  1952. n_codes = len(codes_idx)
  1953. if n_codes == 0:
  1954. continue
  1955. # Separate codes in line
  1956. parts = []
  1957. for p in range(n_codes-1):
  1958. parts.append(line[codes_idx[p]:codes_idx[p+1]].strip())
  1959. parts.append(line[codes_idx[-1]:].strip())
  1960. # Separate codes from values
  1961. cmds = {}
  1962. for part in parts:
  1963. cmds[part[0]] = float(part[1:])
  1964. gcmds.append(cmds)
  1965. return gcmds
  1966. def gcode_parse(self):
  1967. """
  1968. G-Code parser (from self.gcode). Generates dictionary with
  1969. single-segment LineString's and "kind" indicating cut or travel,
  1970. fast or feedrate speed.
  1971. """
  1972. kind = ["C", "F"] # T=travel, C=cut, F=fast, S=slow
  1973. # Results go here
  1974. geometry = []
  1975. # TODO: Merge into single parser?
  1976. gobjs = self.pre_parse(self.gcode)
  1977. # Last known instruction
  1978. current = {'X': 0.0, 'Y': 0.0, 'Z': 0.0, 'G': 0}
  1979. # Current path: temporary storage until tool is
  1980. # lifted or lowered.
  1981. path = [(0, 0)]
  1982. # Process every instruction
  1983. for gobj in gobjs:
  1984. ## Changing height
  1985. if 'Z' in gobj:
  1986. if ('X' in gobj or 'Y' in gobj) and gobj['Z'] != current['Z']:
  1987. log.warning("Non-orthogonal motion: From %s" % str(current))
  1988. log.warning(" To: %s" % str(gobj))
  1989. current['Z'] = gobj['Z']
  1990. # Store the path into geometry and reset path
  1991. if len(path) > 1:
  1992. geometry.append({"geom": LineString(path),
  1993. "kind": kind})
  1994. path = [path[-1]] # Start with the last point of last path.
  1995. if 'G' in gobj:
  1996. current['G'] = int(gobj['G'])
  1997. if 'X' in gobj or 'Y' in gobj:
  1998. if 'X' in gobj:
  1999. x = gobj['X']
  2000. else:
  2001. x = current['X']
  2002. if 'Y' in gobj:
  2003. y = gobj['Y']
  2004. else:
  2005. y = current['Y']
  2006. kind = ["C", "F"] # T=travel, C=cut, F=fast, S=slow
  2007. if current['Z'] > 0:
  2008. kind[0] = 'T'
  2009. if current['G'] > 0:
  2010. kind[1] = 'S'
  2011. arcdir = [None, None, "cw", "ccw"]
  2012. if current['G'] in [0, 1]: # line
  2013. path.append((x, y))
  2014. if current['G'] in [2, 3]: # arc
  2015. center = [gobj['I'] + current['X'], gobj['J'] + current['Y']]
  2016. radius = sqrt(gobj['I']**2 + gobj['J']**2)
  2017. start = arctan2(-gobj['J'], -gobj['I'])
  2018. stop = arctan2(-center[1]+y, -center[0]+x)
  2019. path += arc(center, radius, start, stop,
  2020. arcdir[current['G']],
  2021. self.steps_per_circ)
  2022. # Update current instruction
  2023. for code in gobj:
  2024. current[code] = gobj[code]
  2025. # There might not be a change in height at the
  2026. # end, therefore, see here too if there is
  2027. # a final path.
  2028. if len(path) > 1:
  2029. geometry.append({"geom": LineString(path),
  2030. "kind": kind})
  2031. self.gcode_parsed = geometry
  2032. return geometry
  2033. # def plot(self, tooldia=None, dpi=75, margin=0.1,
  2034. # color={"T": ["#F0E24D", "#B5AB3A"], "C": ["#5E6CFF", "#4650BD"]},
  2035. # alpha={"T": 0.3, "C": 1.0}):
  2036. # """
  2037. # Creates a Matplotlib figure with a plot of the
  2038. # G-code job.
  2039. # """
  2040. # if tooldia is None:
  2041. # tooldia = self.tooldia
  2042. #
  2043. # fig = Figure(dpi=dpi)
  2044. # ax = fig.add_subplot(111)
  2045. # ax.set_aspect(1)
  2046. # xmin, ymin, xmax, ymax = self.input_geometry_bounds
  2047. # ax.set_xlim(xmin-margin, xmax+margin)
  2048. # ax.set_ylim(ymin-margin, ymax+margin)
  2049. #
  2050. # if tooldia == 0:
  2051. # for geo in self.gcode_parsed:
  2052. # linespec = '--'
  2053. # linecolor = color[geo['kind'][0]][1]
  2054. # if geo['kind'][0] == 'C':
  2055. # linespec = 'k-'
  2056. # x, y = geo['geom'].coords.xy
  2057. # ax.plot(x, y, linespec, color=linecolor)
  2058. # else:
  2059. # for geo in self.gcode_parsed:
  2060. # poly = geo['geom'].buffer(tooldia/2.0)
  2061. # patch = PolygonPatch(poly, facecolor=color[geo['kind'][0]][0],
  2062. # edgecolor=color[geo['kind'][0]][1],
  2063. # alpha=alpha[geo['kind'][0]], zorder=2)
  2064. # ax.add_patch(patch)
  2065. #
  2066. # return fig
  2067. def plot2(self, axes, tooldia=None, dpi=75, margin=0.1,
  2068. color={"T": ["#F0E24D", "#B5AB3A"], "C": ["#5E6CFF", "#4650BD"]},
  2069. alpha={"T": 0.3, "C": 1.0}, tool_tolerance=0.0005):
  2070. """
  2071. Plots the G-code job onto the given axes.
  2072. :param axes: Matplotlib axes on which to plot.
  2073. :param tooldia: Tool diameter.
  2074. :param dpi: Not used!
  2075. :param margin: Not used!
  2076. :param color: Color specification.
  2077. :param alpha: Transparency specification.
  2078. :param tool_tolerance: Tolerance when drawing the toolshape.
  2079. :return: None
  2080. """
  2081. path_num = 0
  2082. if tooldia is None:
  2083. tooldia = self.tooldia
  2084. if tooldia == 0:
  2085. for geo in self.gcode_parsed:
  2086. linespec = '--'
  2087. linecolor = color[geo['kind'][0]][1]
  2088. if geo['kind'][0] == 'C':
  2089. linespec = 'k-'
  2090. x, y = geo['geom'].coords.xy
  2091. axes.plot(x, y, linespec, color=linecolor)
  2092. else:
  2093. for geo in self.gcode_parsed:
  2094. path_num += 1
  2095. axes.annotate(str(path_num), xy=geo['geom'].coords[0],
  2096. xycoords='data')
  2097. poly = geo['geom'].buffer(tooldia / 2.0).simplify(tool_tolerance)
  2098. patch = PolygonPatch(poly, facecolor=color[geo['kind'][0]][0],
  2099. edgecolor=color[geo['kind'][0]][1],
  2100. alpha=alpha[geo['kind'][0]], zorder=2)
  2101. axes.add_patch(patch)
  2102. def create_geometry(self):
  2103. # TODO: This takes forever. Too much data?
  2104. self.solid_geometry = cascaded_union([geo['geom'] for geo in self.gcode_parsed])
  2105. def polygon2gcode(self, polygon, tolerance=0):
  2106. """
  2107. Creates G-Code for the exterior and all interior paths
  2108. of a polygon.
  2109. :param polygon: A Shapely.Polygon
  2110. :type polygon: Shapely.Polygon
  2111. :param tolerance: All points in the simplified object will be within the
  2112. tolerance distance of the original geometry.
  2113. :type tolerance: float
  2114. :return: G-code to cut along polygon.
  2115. :rtype: str
  2116. """
  2117. if tolerance > 0:
  2118. target_polygon = polygon.simplify(tolerance)
  2119. else:
  2120. target_polygon = polygon
  2121. gcode = ""
  2122. t = "G0%d X%.4fY%.4f\n"
  2123. path = list(target_polygon.exterior.coords) # Polygon exterior
  2124. gcode += t % (0, path[0][0], path[0][1]) # Move to first point
  2125. if self.zdownrate is not None:
  2126. gcode += "F%.2f\n" % self.zdownrate
  2127. gcode += "G01 Z%.4f\n" % self.z_cut # Start cutting
  2128. gcode += "F%.2f\n" % self.feedrate
  2129. else:
  2130. gcode += "G01 Z%.4f\n" % self.z_cut # Start cutting
  2131. for pt in path[1:]:
  2132. gcode += t % (1, pt[0], pt[1]) # Linear motion to point
  2133. gcode += "G00 Z%.4f\n" % self.z_move # Stop cutting
  2134. for ints in target_polygon.interiors: # Polygon interiors
  2135. path = list(ints.coords)
  2136. gcode += t % (0, path[0][0], path[0][1]) # Move to first point
  2137. if self.zdownrate is not None:
  2138. gcode += "F%.2f\n" % self.zdownrate
  2139. gcode += "G01 Z%.4f\n" % self.z_cut # Start cutting
  2140. gcode += "F%.2f\n" % self.feedrate
  2141. else:
  2142. gcode += "G01 Z%.4f\n" % self.z_cut # Start cutting
  2143. for pt in path[1:]:
  2144. gcode += t % (1, pt[0], pt[1]) # Linear motion to point
  2145. gcode += "G00 Z%.4f\n" % self.z_move # Stop cutting
  2146. return gcode
  2147. def linear2gcode(self, linear, tolerance=0):
  2148. """
  2149. Generates G-code to cut along the linear feature.
  2150. :param linear: The path to cut along.
  2151. :type: Shapely.LinearRing or Shapely.Linear String
  2152. :param tolerance: All points in the simplified object will be within the
  2153. tolerance distance of the original geometry.
  2154. :type tolerance: float
  2155. :return: G-code to cut alon the linear feature.
  2156. :rtype: str
  2157. """
  2158. if tolerance > 0:
  2159. target_linear = linear.simplify(tolerance)
  2160. else:
  2161. target_linear = linear
  2162. gcode = ""
  2163. t = "G0%d X%.4fY%.4f\n"
  2164. path = list(target_linear.coords)
  2165. gcode += t % (0, path[0][0], path[0][1]) # Move to first point
  2166. if self.zdownrate is not None:
  2167. gcode += "F%.2f\n" % self.zdownrate
  2168. gcode += "G01 Z%.4f\n" % self.z_cut # Start cutting
  2169. gcode += "F%.2f\n" % self.feedrate
  2170. else:
  2171. gcode += "G01 Z%.4f\n" % self.z_cut # Start cutting
  2172. for pt in path[1:]:
  2173. gcode += t % (1, pt[0], pt[1]) # Linear motion to point
  2174. gcode += "G00 Z%.4f\n" % self.z_move # Stop cutting
  2175. return gcode
  2176. def point2gcode(self, point):
  2177. gcode = ""
  2178. t = "G0%d X%.4fY%.4f\n"
  2179. path = list(point.coords)
  2180. gcode += t % (0, path[0][0], path[0][1]) # Move to first point
  2181. if self.zdownrate is not None:
  2182. gcode += "F%.2f\n" % self.zdownrate
  2183. gcode += "G01 Z%.4f\n" % self.z_cut # Start cutting
  2184. gcode += "F%.2f\n" % self.feedrate
  2185. else:
  2186. gcode += "G01 Z%.4f\n" % self.z_cut # Start cutting
  2187. gcode += "G00 Z%.4f\n" % self.z_move # Stop cutting
  2188. return gcode
  2189. def scale(self, factor):
  2190. """
  2191. Scales all the geometry on the XY plane in the object by the
  2192. given factor. Tool sizes, feedrates, or Z-axis dimensions are
  2193. not altered.
  2194. :param factor: Number by which to scale the object.
  2195. :type factor: float
  2196. :return: None
  2197. :rtype: None
  2198. """
  2199. for g in self.gcode_parsed:
  2200. g['geom'] = affinity.scale(g['geom'], factor, factor, origin=(0, 0))
  2201. self.create_geometry()
  2202. def offset(self, vect):
  2203. """
  2204. Offsets all the geometry on the XY plane in the object by the
  2205. given vector.
  2206. :param vect: (x, y) offset vector.
  2207. :type vect: tuple
  2208. :return: None
  2209. """
  2210. dx, dy = vect
  2211. for g in self.gcode_parsed:
  2212. g['geom'] = affinity.translate(g['geom'], xoff=dx, yoff=dy)
  2213. self.create_geometry()
  2214. # def get_bounds(geometry_set):
  2215. # xmin = Inf
  2216. # ymin = Inf
  2217. # xmax = -Inf
  2218. # ymax = -Inf
  2219. #
  2220. # #print "Getting bounds of:", str(geometry_set)
  2221. # for gs in geometry_set:
  2222. # try:
  2223. # gxmin, gymin, gxmax, gymax = geometry_set[gs].bounds()
  2224. # xmin = min([xmin, gxmin])
  2225. # ymin = min([ymin, gymin])
  2226. # xmax = max([xmax, gxmax])
  2227. # ymax = max([ymax, gymax])
  2228. # except:
  2229. # print "DEV WARNING: Tried to get bounds of empty geometry."
  2230. #
  2231. # return [xmin, ymin, xmax, ymax]
  2232. def get_bounds(geometry_list):
  2233. xmin = Inf
  2234. ymin = Inf
  2235. xmax = -Inf
  2236. ymax = -Inf
  2237. #print "Getting bounds of:", str(geometry_set)
  2238. for gs in geometry_list:
  2239. try:
  2240. gxmin, gymin, gxmax, gymax = gs.bounds()
  2241. xmin = min([xmin, gxmin])
  2242. ymin = min([ymin, gymin])
  2243. xmax = max([xmax, gxmax])
  2244. ymax = max([ymax, gymax])
  2245. except:
  2246. log.warning("DEVELOPMENT: Tried to get bounds of empty geometry.")
  2247. return [xmin, ymin, xmax, ymax]
  2248. def arc(center, radius, start, stop, direction, steps_per_circ):
  2249. """
  2250. Creates a list of point along the specified arc.
  2251. :param center: Coordinates of the center [x, y]
  2252. :type center: list
  2253. :param radius: Radius of the arc.
  2254. :type radius: float
  2255. :param start: Starting angle in radians
  2256. :type start: float
  2257. :param stop: End angle in radians
  2258. :type stop: float
  2259. :param direction: Orientation of the arc, "CW" or "CCW"
  2260. :type direction: string
  2261. :param steps_per_circ: Number of straight line segments to
  2262. represent a circle.
  2263. :type steps_per_circ: int
  2264. :return: The desired arc, as list of tuples
  2265. :rtype: list
  2266. """
  2267. # TODO: Resolution should be established by fraction of total length, not angle.
  2268. da_sign = {"cw": -1.0, "ccw": 1.0}
  2269. points = []
  2270. if direction == "ccw" and stop <= start:
  2271. stop += 2 * pi
  2272. if direction == "cw" and stop >= start:
  2273. stop -= 2 * pi
  2274. angle = abs(stop - start)
  2275. #angle = stop-start
  2276. steps = max([int(ceil(angle / (2 * pi) * steps_per_circ)), 2])
  2277. delta_angle = da_sign[direction] * angle * 1.0 / steps
  2278. for i in range(steps + 1):
  2279. theta = start + delta_angle * i
  2280. points.append((center[0] + radius * cos(theta), center[1] + radius * sin(theta)))
  2281. return points
  2282. def arc2(p1, p2, center, direction, steps_per_circ):
  2283. r = sqrt((center[0] - p1[0]) ** 2 + (center[1] - p1[1]) ** 2)
  2284. start = arctan2(p1[1] - center[1], p1[0] - center[0])
  2285. stop = arctan2(p2[1] - center[1], p2[0] - center[0])
  2286. return arc(center, r, start, stop, direction, steps_per_circ)
  2287. def arc_angle(start, stop, direction):
  2288. if direction == "ccw" and stop <= start:
  2289. stop += 2 * pi
  2290. if direction == "cw" and stop >= start:
  2291. stop -= 2 * pi
  2292. angle = abs(stop - start)
  2293. return angle
  2294. # def clear_poly(poly, tooldia, overlap=0.1):
  2295. # """
  2296. # Creates a list of Shapely geometry objects covering the inside
  2297. # of a Shapely.Polygon. Use for removing all the copper in a region
  2298. # or bed flattening.
  2299. #
  2300. # :param poly: Target polygon
  2301. # :type poly: Shapely.Polygon
  2302. # :param tooldia: Diameter of the tool
  2303. # :type tooldia: float
  2304. # :param overlap: Fraction of the tool diameter to overlap
  2305. # in each pass.
  2306. # :type overlap: float
  2307. # :return: list of Shapely.Polygon
  2308. # :rtype: list
  2309. # """
  2310. # poly_cuts = [poly.buffer(-tooldia/2.0)]
  2311. # while True:
  2312. # poly = poly_cuts[-1].buffer(-tooldia*(1-overlap))
  2313. # if poly.area > 0:
  2314. # poly_cuts.append(poly)
  2315. # else:
  2316. # break
  2317. # return poly_cuts
  2318. def find_polygon(poly_set, point):
  2319. """
  2320. Return the first polygon in the list of polygons poly_set
  2321. that contains the given point.
  2322. """
  2323. p = Point(point)
  2324. for poly in poly_set:
  2325. if poly.contains(p):
  2326. return poly
  2327. return None
  2328. def to_dict(obj):
  2329. """
  2330. Makes the following types into serializable form:
  2331. * ApertureMacro
  2332. * BaseGeometry
  2333. :param obj: Shapely geometry.
  2334. :type obj: BaseGeometry
  2335. :return: Dictionary with serializable form if ``obj`` was
  2336. BaseGeometry or ApertureMacro, otherwise returns ``obj``.
  2337. """
  2338. if isinstance(obj, ApertureMacro):
  2339. return {
  2340. "__class__": "ApertureMacro",
  2341. "__inst__": obj.to_dict()
  2342. }
  2343. if isinstance(obj, BaseGeometry):
  2344. return {
  2345. "__class__": "Shply",
  2346. "__inst__": sdumps(obj)
  2347. }
  2348. return obj
  2349. def dict2obj(d):
  2350. """
  2351. Default deserializer.
  2352. :param d: Serializable dictionary representation of an object
  2353. to be reconstructed.
  2354. :return: Reconstructed object.
  2355. """
  2356. if '__class__' in d and '__inst__' in d:
  2357. if d['__class__'] == "Shply":
  2358. return sloads(d['__inst__'])
  2359. if d['__class__'] == "ApertureMacro":
  2360. am = ApertureMacro()
  2361. am.from_dict(d['__inst__'])
  2362. return am
  2363. return d
  2364. else:
  2365. return d
  2366. def plotg(geo, solid_poly=False):
  2367. try:
  2368. _ = iter(geo)
  2369. except:
  2370. geo = [geo]
  2371. for g in geo:
  2372. if type(g) == Polygon:
  2373. if solid_poly:
  2374. patch = PolygonPatch(g,
  2375. facecolor="#BBF268",
  2376. edgecolor="#006E20",
  2377. alpha=0.75,
  2378. zorder=2)
  2379. ax = subplot(111)
  2380. ax.add_patch(patch)
  2381. else:
  2382. x, y = g.exterior.coords.xy
  2383. plot(x, y)
  2384. for ints in g.interiors:
  2385. x, y = ints.coords.xy
  2386. plot(x, y)
  2387. continue
  2388. if type(g) == LineString or type(g) == LinearRing:
  2389. x, y = g.coords.xy
  2390. plot(x, y)
  2391. continue
  2392. if type(g) == Point:
  2393. x, y = g.coords.xy
  2394. plot(x, y, 'o')
  2395. continue
  2396. try:
  2397. _ = iter(g)
  2398. plotg(g)
  2399. except:
  2400. log.error("Cannot plot: " + str(type(g)))
  2401. continue
  2402. def parse_gerber_number(strnumber, frac_digits):
  2403. """
  2404. Parse a single number of Gerber coordinates.
  2405. :param strnumber: String containing a number in decimal digits
  2406. from a coordinate data block, possibly with a leading sign.
  2407. :type strnumber: str
  2408. :param frac_digits: Number of digits used for the fractional
  2409. part of the number
  2410. :type frac_digits: int
  2411. :return: The number in floating point.
  2412. :rtype: float
  2413. """
  2414. return int(strnumber)*(10**(-frac_digits))
  2415. def voronoi(P):
  2416. """
  2417. Returns a list of all edges of the voronoi diagram for the given input points.
  2418. """
  2419. delauny = Delaunay(P)
  2420. triangles = delauny.points[delauny.vertices]
  2421. circum_centers = np.array([triangle_csc(tri) for tri in triangles])
  2422. long_lines_endpoints = []
  2423. lineIndices = []
  2424. for i, triangle in enumerate(triangles):
  2425. circum_center = circum_centers[i]
  2426. for j, neighbor in enumerate(delauny.neighbors[i]):
  2427. if neighbor != -1:
  2428. lineIndices.append((i, neighbor))
  2429. else:
  2430. ps = triangle[(j+1)%3] - triangle[(j-1)%3]
  2431. ps = np.array((ps[1], -ps[0]))
  2432. middle = (triangle[(j+1)%3] + triangle[(j-1)%3]) * 0.5
  2433. di = middle - triangle[j]
  2434. ps /= np.linalg.norm(ps)
  2435. di /= np.linalg.norm(di)
  2436. if np.dot(di, ps) < 0.0:
  2437. ps *= -1000.0
  2438. else:
  2439. ps *= 1000.0
  2440. long_lines_endpoints.append(circum_center + ps)
  2441. lineIndices.append((i, len(circum_centers) + len(long_lines_endpoints)-1))
  2442. vertices = np.vstack((circum_centers, long_lines_endpoints))
  2443. # filter out any duplicate lines
  2444. lineIndicesSorted = np.sort(lineIndices) # make (1,2) and (2,1) both (1,2)
  2445. lineIndicesTupled = [tuple(row) for row in lineIndicesSorted]
  2446. lineIndicesUnique = np.unique(lineIndicesTupled)
  2447. return vertices, lineIndicesUnique
  2448. def triangle_csc(pts):
  2449. rows, cols = pts.shape
  2450. A = np.bmat([[2 * np.dot(pts, pts.T), np.ones((rows, 1))],
  2451. [np.ones((1, rows)), np.zeros((1, 1))]])
  2452. b = np.hstack((np.sum(pts * pts, axis=1), np.ones((1))))
  2453. x = np.linalg.solve(A,b)
  2454. bary_coords = x[:-1]
  2455. return np.sum(pts * np.tile(bary_coords.reshape((pts.shape[0], 1)), (1, pts.shape[1])), axis=0)
  2456. def voronoi_cell_lines(points, vertices, lineIndices):
  2457. """
  2458. Returns a mapping from a voronoi cell to its edges.
  2459. :param points: shape (m,2)
  2460. :param vertices: shape (n,2)
  2461. :param lineIndices: shape (o,2)
  2462. :rtype: dict point index -> list of shape (n,2) with vertex indices
  2463. """
  2464. kd = KDTree(points)
  2465. cells = collections.defaultdict(list)
  2466. for i1, i2 in lineIndices:
  2467. v1, v2 = vertices[i1], vertices[i2]
  2468. mid = (v1+v2)/2
  2469. _, (p1Idx, p2Idx) = kd.query(mid, 2)
  2470. cells[p1Idx].append((i1, i2))
  2471. cells[p2Idx].append((i1, i2))
  2472. return cells
  2473. def voronoi_edges2polygons(cells):
  2474. """
  2475. Transforms cell edges into polygons.
  2476. :param cells: as returned from voronoi_cell_lines
  2477. :rtype: dict point index -> list of vertex indices which form a polygon
  2478. """
  2479. # first, close the outer cells
  2480. for pIdx, lineIndices_ in cells.items():
  2481. dangling_lines = []
  2482. for i1, i2 in lineIndices_:
  2483. connections = filter(lambda (i1_, i2_): (i1, i2) != (i1_, i2_) and (i1 == i1_ or i1 == i2_ or i2 == i1_ or i2 == i2_), lineIndices_)
  2484. assert 1 <= len(connections) <= 2
  2485. if len(connections) == 1:
  2486. dangling_lines.append((i1, i2))
  2487. assert len(dangling_lines) in [0, 2]
  2488. if len(dangling_lines) == 2:
  2489. (i11, i12), (i21, i22) = dangling_lines
  2490. # determine which line ends are unconnected
  2491. connected = filter(lambda (i1,i2): (i1,i2) != (i11,i12) and (i1 == i11 or i2 == i11), lineIndices_)
  2492. i11Unconnected = len(connected) == 0
  2493. connected = filter(lambda (i1,i2): (i1,i2) != (i21,i22) and (i1 == i21 or i2 == i21), lineIndices_)
  2494. i21Unconnected = len(connected) == 0
  2495. startIdx = i11 if i11Unconnected else i12
  2496. endIdx = i21 if i21Unconnected else i22
  2497. cells[pIdx].append((startIdx, endIdx))
  2498. # then, form polygons by storing vertex indices in (counter-)clockwise order
  2499. polys = dict()
  2500. for pIdx, lineIndices_ in cells.items():
  2501. # get a directed graph which contains both directions and arbitrarily follow one of both
  2502. directedGraph = lineIndices_ + [(i2, i1) for (i1, i2) in lineIndices_]
  2503. directedGraphMap = collections.defaultdict(list)
  2504. for (i1, i2) in directedGraph:
  2505. directedGraphMap[i1].append(i2)
  2506. orderedEdges = []
  2507. currentEdge = directedGraph[0]
  2508. while len(orderedEdges) < len(lineIndices_):
  2509. i1 = currentEdge[1]
  2510. i2 = directedGraphMap[i1][0] if directedGraphMap[i1][0] != currentEdge[0] else directedGraphMap[i1][1]
  2511. nextEdge = (i1, i2)
  2512. orderedEdges.append(nextEdge)
  2513. currentEdge = nextEdge
  2514. polys[pIdx] = [i1 for (i1, i2) in orderedEdges]
  2515. return polys
  2516. def voronoi_polygons(points):
  2517. """
  2518. Returns the voronoi polygon for each input point.
  2519. :param points: shape (n,2)
  2520. :rtype: list of n polygons where each polygon is an array of vertices
  2521. """
  2522. vertices, lineIndices = voronoi(points)
  2523. cells = voronoi_cell_lines(points, vertices, lineIndices)
  2524. polys = voronoi_edges2polygons(cells)
  2525. polylist = []
  2526. for i in xrange(len(points)):
  2527. poly = vertices[np.asarray(polys[i])]
  2528. polylist.append(poly)
  2529. return polylist
  2530. class Zprofile:
  2531. def __init__(self):
  2532. # data contains lists of [x, y, z]
  2533. self.data = []
  2534. # Computed voronoi polygons (shapely)
  2535. self.polygons = []
  2536. pass
  2537. def plot_polygons(self):
  2538. axes = plt.subplot(1, 1, 1)
  2539. plt.axis([-0.05, 1.05, -0.05, 1.05])
  2540. for poly in self.polygons:
  2541. p = PolygonPatch(poly, facecolor=np.random.rand(3, 1), alpha=0.3)
  2542. axes.add_patch(p)
  2543. def init_from_csv(self, filename):
  2544. pass
  2545. def init_from_string(self, zpstring):
  2546. pass
  2547. def init_from_list(self, zplist):
  2548. self.data = zplist
  2549. def generate_polygons(self):
  2550. self.polygons = [Polygon(p) for p in voronoi_polygons(array([[x[0], x[1]] for x in self.data]))]
  2551. def normalize(self, origin):
  2552. pass
  2553. def paste(self, path):
  2554. """
  2555. Return a list of dictionaries containing the parts of the original
  2556. path and their z-axis offset.
  2557. """
  2558. # At most one region/polygon will contain the path
  2559. containing = [i for i in range(len(self.polygons)) if self.polygons[i].contains(path)]
  2560. if len(containing) > 0:
  2561. return [{"path": path, "z": self.data[containing[0]][2]}]
  2562. # All region indexes that intersect with the path
  2563. crossing = [i for i in range(len(self.polygons)) if self.polygons[i].intersects(path)]
  2564. return [{"path": path.intersection(self.polygons[i]),
  2565. "z": self.data[i][2]} for i in crossing]
  2566. def autolist(obj):
  2567. try:
  2568. _ = iter(obj)
  2569. return obj
  2570. except TypeError:
  2571. return [obj]
  2572. def three_point_circle(p1, p2, p3):
  2573. """
  2574. Computes the center and radius of a circle from
  2575. 3 points on its circumference.
  2576. :param p1: Point 1
  2577. :param p2: Point 2
  2578. :param p3: Point 3
  2579. :return: center, radius
  2580. """
  2581. # Midpoints
  2582. a1 = (p1 + p2) / 2.0
  2583. a2 = (p2 + p3) / 2.0
  2584. # Normals
  2585. b1 = dot((p2 - p1), array([[0, -1], [1, 0]], dtype=float32))
  2586. b2 = dot((p3 - p2), array([[0, 1], [-1, 0]], dtype=float32))
  2587. # Params
  2588. T = solve(transpose(array([-b1, b2])), a1 - a2)
  2589. print T
  2590. # Center
  2591. center = a1 + b1 * T[0]
  2592. # Radius
  2593. radius = norm(center - p1)
  2594. return center, radius, T[0]
  2595. def distance(pt1, pt2):
  2596. return sqrt((pt1[0] - pt2[0]) ** 2 + (pt1[1] - pt2[1]) ** 2)
  2597. class FlatCAMRTree(object):
  2598. def __init__(self):
  2599. self.rti = rtindex.Index()
  2600. self.obj2points = []
  2601. self.points2obj = []
  2602. self.get_points = lambda go: go.coords
  2603. def grow_obj2points(self, idx):
  2604. if len(self.obj2points) > idx:
  2605. # len == 2, idx == 1, ok.
  2606. return
  2607. else:
  2608. # len == 2, idx == 2, need 1 more.
  2609. # range(2, 3)
  2610. for i in range(len(self.obj2points), idx + 1):
  2611. self.obj2points.append([])
  2612. def insert(self, objid, obj):
  2613. self.grow_obj2points(objid)
  2614. self.obj2points[objid] = []
  2615. #for pt in obj.coords:
  2616. for pt in self.get_points(obj):
  2617. self.rti.insert(len(self.points2obj), (pt[0], pt[1], pt[0], pt[1]), obj=objid)
  2618. self.obj2points[objid].append(len(self.points2obj))
  2619. self.points2obj.append(objid)
  2620. def remove_obj(self, objid, obj):
  2621. # Use all ptids to delete from index
  2622. #for i in range(len(self.obj2points[objid])):
  2623. for i, pt in enumerate(self.get_points(obj)):
  2624. #pt = obj.coords[i]
  2625. #pt = self.get_points(obj)[i]
  2626. self.rti.delete(self.obj2points[objid][i], (pt[0], pt[1], pt[0], pt[1]))
  2627. def nearest(self, pt):
  2628. return self.rti.nearest(pt, objects=True).next()
  2629. class FlatCAMRTreeStorage(FlatCAMRTree):
  2630. def __init__(self):
  2631. super(FlatCAMRTreeStorage, self).__init__()
  2632. self.objects = []
  2633. def insert(self, obj):
  2634. self.objects.append(obj)
  2635. super(FlatCAMRTreeStorage, self).insert(len(self.objects) - 1, obj)
  2636. def remove(self, obj):
  2637. objidx = self.objects.index(obj)
  2638. self.objects[objidx] = None
  2639. self.remove_obj(objidx, obj)
  2640. def get_objects(self):
  2641. return (o for o in self.objects if o is not None)
  2642. def nearest(self, pt):
  2643. """
  2644. Returns the nearest matching points and the object
  2645. it belongs to.
  2646. :param pt: Query point.
  2647. :return: (match_x, match_y), Object owner of
  2648. matching point.
  2649. :rtype: tuple
  2650. """
  2651. tidx = super(FlatCAMRTreeStorage, self).nearest(pt)
  2652. return (tidx.bbox[0], tidx.bbox[1]), self.objects[tidx.object]
  2653. class myO:
  2654. def __init__(self, coords):
  2655. self.coords = coords
  2656. def test_rti():
  2657. o1 = myO([(0, 0), (0, 1), (1, 1)])
  2658. o2 = myO([(2, 0), (2, 1), (2, 1)])
  2659. o3 = myO([(2, 0), (2, 1), (3, 1)])
  2660. os = [o1, o2]
  2661. idx = FlatCAMRTree()
  2662. for o in range(len(os)):
  2663. idx.insert(o, os[o])
  2664. print [x.bbox for x in idx.rti.nearest((0, 0), num_results=20, objects=True)]
  2665. idx.remove_obj(0, o1)
  2666. print [x.bbox for x in idx.rti.nearest((0, 0), num_results=20, objects=True)]
  2667. idx.remove_obj(1, o2)
  2668. print [x.bbox for x in idx.rti.nearest((0, 0), num_results=20, objects=True)]
  2669. def test_rtis():
  2670. o1 = myO([(0, 0), (0, 1), (1, 1)])
  2671. o2 = myO([(2, 0), (2, 1), (2, 1)])
  2672. o3 = myO([(2, 0), (2, 1), (3, 1)])
  2673. os = [o1, o2]
  2674. idx = FlatCAMRTreeStorage()
  2675. for o in range(len(os)):
  2676. idx.insert(os[o])
  2677. #os = None
  2678. #o1 = None
  2679. #o2 = None
  2680. print [x.bbox for x in idx.rti.nearest((0, 0), num_results=20, objects=True)]
  2681. idx.remove(idx.nearest((2,0))[1])
  2682. print [x.bbox for x in idx.rti.nearest((0, 0), num_results=20, objects=True)]
  2683. idx.remove(idx.nearest((0,0))[1])
  2684. print [x.bbox for x in idx.rti.nearest((0, 0), num_results=20, objects=True)]