camlib.py 52 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513
  1. ############################################################
  2. # FlatCAM: 2D Post-processing for Manufacturing #
  3. # http://caram.cl/software/flatcam #
  4. # Author: Juan Pablo Caram (c) #
  5. # Date: 2/5/2014 #
  6. # MIT Licence #
  7. ############################################################
  8. from numpy import arctan2, Inf, array, sqrt, pi, ceil, sin, cos
  9. from matplotlib.figure import Figure
  10. import re
  11. # See: http://toblerity.org/shapely/manual.html
  12. from shapely.geometry import Polygon, LineString, Point, LinearRing
  13. from shapely.geometry import MultiPoint, MultiPolygon
  14. from shapely.geometry import box as shply_box
  15. from shapely.ops import cascaded_union
  16. import shapely.affinity as affinity
  17. from shapely.wkt import loads as sloads
  18. from shapely.wkt import dumps as sdumps
  19. from shapely.geometry.base import BaseGeometry
  20. # Used for solid polygons in Matplotlib
  21. from descartes.patch import PolygonPatch
  22. import simplejson as json
  23. # TODO: Commented for FlatCAM packaging with cx_freeze
  24. #from matplotlib.pyplot import plot
  25. class Geometry:
  26. def __init__(self):
  27. # Units (in or mm)
  28. self.units = 'in'
  29. # Final geometry: MultiPolygon
  30. self.solid_geometry = None
  31. # Attributes to be included in serialization
  32. self.ser_attrs = ['units', 'solid_geometry']
  33. def isolation_geometry(self, offset):
  34. """
  35. Creates contours around geometry at a given
  36. offset distance.
  37. """
  38. return self.solid_geometry.buffer(offset)
  39. def bounds(self):
  40. """
  41. Returns coordinates of rectangular bounds
  42. of geometry: (xmin, ymin, xmax, ymax).
  43. """
  44. if self.solid_geometry is None:
  45. print "Warning: solid_geometry not computed yet."
  46. return (0, 0, 0, 0)
  47. if type(self.solid_geometry) == list:
  48. # TODO: This can be done faster. See comment from Shapely mailing lists.
  49. return cascaded_union(self.solid_geometry).bounds
  50. else:
  51. return self.solid_geometry.bounds
  52. def size(self):
  53. """
  54. Returns (width, height) of rectangular
  55. bounds of geometry.
  56. """
  57. if self.solid_geometry is None:
  58. print "Warning: solid_geometry not computed yet."
  59. return 0
  60. bounds = self.bounds()
  61. return (bounds[2]-bounds[0], bounds[3]-bounds[1])
  62. def get_empty_area(self, boundary=None):
  63. """
  64. Returns the complement of self.solid_geometry within
  65. the given boundary polygon. If not specified, it defaults to
  66. the rectangular bounding box of self.solid_geometry.
  67. """
  68. if boundary is None:
  69. boundary = self.solid_geometry.envelope
  70. return boundary.difference(self.solid_geometry)
  71. def clear_polygon(self, polygon, tooldia, overlap=0.15):
  72. """
  73. Creates geometry inside a polygon for a tool to cover
  74. the whole area.
  75. """
  76. poly_cuts = [polygon.buffer(-tooldia/2.0)]
  77. while True:
  78. polygon = poly_cuts[-1].buffer(-tooldia*(1-overlap))
  79. if polygon.area > 0:
  80. poly_cuts.append(polygon)
  81. else:
  82. break
  83. return poly_cuts
  84. def scale(self, factor):
  85. """
  86. Scales all of the object's geometry by a given factor. Override
  87. this method.
  88. :param factor: Number by which to scale.
  89. :type factor: float
  90. :return: None
  91. :rtype: None
  92. """
  93. return
  94. def convert_units(self, units):
  95. """
  96. Converts the units of the object to ``units`` by scaling all
  97. the geometry appropriately. This call ``scale()``. Don't call
  98. it again in descendents.
  99. :param units: "IN" or "MM"
  100. :type units: str
  101. :return: Scaling factor resulting from unit change.
  102. :rtype: float
  103. """
  104. print "Geometry.convert_units()"
  105. if units.upper() == self.units.upper():
  106. return 1.0
  107. if units.upper() == "MM":
  108. factor = 25.4
  109. elif units.upper() == "IN":
  110. factor = 1/25.4
  111. else:
  112. print "Unsupported units:", units
  113. return 1.0
  114. self.units = units
  115. self.scale(factor)
  116. return factor
  117. def to_dict(self):
  118. """
  119. Returns a respresentation of the object as a dictionary.
  120. Attributes to include are listed in ``self.ser_attrs``.
  121. :return: A dictionary-encoded copy of the object.
  122. :rtype: dict
  123. """
  124. d = {}
  125. for attr in self.ser_attrs:
  126. d[attr] = getattr(self, attr)
  127. return d
  128. def from_dict(self, d):
  129. """
  130. Sets object's attributes from a dictionary.
  131. Attributes to include are listed in ``self.ser_attrs``.
  132. """
  133. for attr in self.ser_attrs:
  134. setattr(self, attr, d[attr])
  135. class Gerber (Geometry):
  136. """
  137. **ATTRIBUTES**
  138. * ``apertures`` (dict): The keys are names/identifiers of each aperture.
  139. The values are dictionaries key/value pairs which describe the aperture. The
  140. type key is always present and the rest depend on the key:
  141. +-----------+-----------------------------------+
  142. | Key | Value |
  143. +===========+===================================+
  144. | type | (str) "C", "R", or "O" |
  145. +-----------+-----------------------------------+
  146. | others | Depend on ``type`` |
  147. +-----------+-----------------------------------+
  148. * ``paths`` (list): A path is described by a line an aperture that follows that
  149. line. Each paths[i] is a dictionary:
  150. +------------+------------------------------------------------+
  151. | Key | Value |
  152. +============+================================================+
  153. | linestring | (Shapely.LineString) The actual path. |
  154. +------------+------------------------------------------------+
  155. | aperture | (str) The key for an aperture in apertures. |
  156. +------------+------------------------------------------------+
  157. * ``flashes`` (list): Flashes are single-point strokes of an aperture. Each
  158. is a dictionary:
  159. +------------+------------------------------------------------+
  160. | Key | Value |
  161. +============+================================================+
  162. | loc | (list) [x (float), y (float)] coordinates. |
  163. +------------+------------------------------------------------+
  164. | aperture | (str) The key for an aperture in apertures. |
  165. +------------+------------------------------------------------+
  166. * ``regions`` (list): Are surfaces defined by a polygon (Shapely.Polygon),
  167. which have an exterior and zero or more interiors. An aperture is also
  168. associated with a region. Each is a dictionary:
  169. +------------+-----------------------------------------------------+
  170. | Key | Value |
  171. +============+=====================================================+
  172. | polygon | (Shapely.Polygon) The polygon defining the region. |
  173. +------------+-----------------------------------------------------+
  174. | aperture | (str) The key for an aperture in apertures. |
  175. +------------+-----------------------------------------------------+
  176. * ``flash_geometry`` (list): List of (Shapely) geometric object resulting
  177. from ``flashes``. These are generated from ``flashes`` in ``do_flashes()``.
  178. * ``buffered_paths`` (list): List of (Shapely) polygons resulting from
  179. *buffering* (or thickening) the ``paths`` with the aperture. These are
  180. generated from ``paths`` in ``buffer_paths()``.
  181. **USAGE**::
  182. g = Gerber()
  183. g.parse_file(filename)
  184. g.create_geometry()
  185. do_something(s.solid_geometry)
  186. """
  187. def __init__(self):
  188. """
  189. The constructor takes no parameters. Use ``gerber.parse_files()``
  190. or ``gerber.parse_lines()`` to populate the object from Gerber source.
  191. :return: Gerber object
  192. :rtype: Gerber
  193. """
  194. # Initialize parent
  195. Geometry.__init__(self)
  196. # Number format
  197. self.int_digits = 3
  198. """Number of integer digits in Gerber numbers. Used during parsing."""
  199. self.frac_digits = 4
  200. """Number of fraction digits in Gerber numbers. Used during parsing."""
  201. ## Gerber elements ##
  202. # Apertures {'id':{'type':chr,
  203. # ['size':float], ['width':float],
  204. # ['height':float]}, ...}
  205. self.apertures = {}
  206. # Paths [{'linestring':LineString, 'aperture':str}]
  207. self.paths = []
  208. # Buffered Paths [Polygon]
  209. # Paths transformed into Polygons by
  210. # offsetting the aperture size/2
  211. self.buffered_paths = []
  212. # Polygon regions [{'polygon':Polygon, 'aperture':str}]
  213. self.regions = []
  214. # Flashes [{'loc':[float,float], 'aperture':str}]
  215. self.flashes = []
  216. # Geometry from flashes
  217. self.flash_geometry = []
  218. # Attributes to be included in serialization
  219. # Always append to it because it carries contents
  220. # from Geometry.
  221. self.ser_attrs += ['int_digits', 'frac_digits', 'apertures', 'paths',
  222. 'buffered_paths', 'regions', 'flashes',
  223. 'flash_geometry']
  224. #### Parser patterns ####
  225. # FS - Format Specification
  226. # The format of X and Y must be the same!
  227. # L-omit leading zeros, T-omit trailing zeros
  228. # A-absolute notation, I-incremental notation
  229. self.fmt_re = re.compile(r'%FS([LT])([AI])X(\d)(\d)Y\d\d\*%$')
  230. # Mode (IN/MM)
  231. self.mode_re = re.compile(r'^%MO(IN|MM)\*%$')
  232. # Comment G04|G4
  233. self.comm_re = re.compile(r'^G0?4(.*)$')
  234. # AD - Aperture definition
  235. self.ad_re = re.compile(r'^%ADD(\d\d+)([a-zA-Z0-9]*),(.*)\*%$')
  236. # AM - Aperture Macro
  237. # Beginning of macro (Ends with *%):
  238. self.am_re = re.compile(r'^%AM([a-zA-Z0-9]*)\*')
  239. # Tool change
  240. # May begin with G54 but that is deprecated
  241. self.tool_re = re.compile(r'^(?:G54)?D(\d\d+)\*$')
  242. # G01 - Linear interpolation plus flashes
  243. # Operation code (D0x) missing is deprecated... oh well I will support it.
  244. self.lin_re = re.compile(r'^(?:G0?(1))?(?:X(-?\d+))?(?:Y(-?\d+))?(?:D0([123]))?\*$')
  245. self.setlin_re = re.compile(r'^(?:G0?1)\*')
  246. # G02/3 - Circular interpolation
  247. # 2-clockwise, 3-counterclockwise
  248. self.circ_re = re.compile(r'^(?:G0?([23]))?(?:X(-?\d+))?(?:Y(-?\d+))' +
  249. '?(?:I(-?\d+))?(?:J(-?\d+))?D0([12])\*$')
  250. # G01/2/3 Occurring without coordinates
  251. self.interp_re = re.compile(r'^(?:G0?([123]))\*')
  252. # Single D74 or multi D75 quadrant for circular interpolation
  253. self.quad_re = re.compile(r'^G7([45])\*$')
  254. # Region mode on
  255. # In region mode, D01 starts a region
  256. # and D02 ends it. A new region can be started again
  257. # with D01. All contours must be closed before
  258. # D02 or G37.
  259. self.regionon_re = re.compile(r'^G36\*$')
  260. # Region mode off
  261. # Will end a region and come off region mode.
  262. # All contours must be closed before D02 or G37.
  263. self.regionoff_re = re.compile(r'^G37\*$')
  264. # End of file
  265. self.eof_re = re.compile(r'^M02\*')
  266. # IP - Image polarity
  267. self.pol_re = re.compile(r'^%IP(POS|NEG)\*%$')
  268. # LP - Level polarity
  269. self.lpol_re = re.compile(r'^%LP([DC])\*%$')
  270. def scale(self, factor):
  271. """
  272. Scales the objects' geometry on the XY plane by a given factor.
  273. These are:
  274. * ``apertures``
  275. * ``paths``
  276. * ``regions``
  277. * ``flashes``
  278. Then ``buffered_paths``, ``flash_geometry`` and ``solid_geometry``
  279. are re-created with ``self.create_geometry()``.
  280. :param factor: Number by which to scale.
  281. :type factor: float
  282. :rtype : None
  283. """
  284. # Apertures
  285. print "Scaling apertures..."
  286. for apid in self.apertures:
  287. for param in self.apertures[apid]:
  288. if param != "type": # All others are dimensions.
  289. print "Tool:", apid, "Parameter:", param
  290. self.apertures[apid][param] *= factor
  291. # Paths
  292. print "Scaling paths..."
  293. for path in self.paths:
  294. path['linestring'] = affinity.scale(path['linestring'],
  295. factor, factor, origin=(0, 0))
  296. # Flashes
  297. print "Scaling flashes..."
  298. for fl in self.flashes:
  299. # TODO: Shouldn't 'loc' be a numpy.array()?
  300. fl['loc'][0] *= factor
  301. fl['loc'][1] *= factor
  302. # Regions
  303. print "Scaling regions..."
  304. for reg in self.regions:
  305. reg['polygon'] = affinity.scale(reg['polygon'], factor, factor,
  306. origin=(0, 0))
  307. # Now buffered_paths, flash_geometry and solid_geometry
  308. self.create_geometry()
  309. def fix_regions(self):
  310. """
  311. Overwrites the region polygons with fixed
  312. versions if found to be invalid (according to Shapely).
  313. """
  314. for region in self.regions:
  315. if not region['polygon'].is_valid:
  316. region['polygon'] = region['polygon'].buffer(0)
  317. def buffer_paths(self):
  318. self.buffered_paths = []
  319. for path in self.paths:
  320. width = self.apertures[path["aperture"]]["size"]
  321. self.buffered_paths.append(path["linestring"].buffer(width/2))
  322. def aperture_parse(self, gline):
  323. """
  324. Parse gerber aperture definition into dictionary of apertures.
  325. The following kinds and their attributes are supported:
  326. * *Circular (C)*: size (float)
  327. * *Rectangle (R)*: width (float), height (float)
  328. * *Obround (O)*: width (float), height (float).
  329. :param gline: Line of Gerber code known to have an aperture definition.
  330. :type gline: str
  331. :return: Identifier of the aperture.
  332. :rtype: str
  333. """
  334. indexstar = gline.find("*")
  335. indexc = gline.find("C,")
  336. if indexc != -1: # Circle, example: %ADD11C,0.1*%
  337. # Found some Gerber with a leading zero in the aperture id and the
  338. # referenced it without the zero, so this is a hack to handle that.
  339. apid = str(int(gline[4:indexc]))
  340. self.apertures[apid] = {"type": "C",
  341. "size": float(gline[indexc+2:indexstar])}
  342. return apid
  343. indexr = gline.find("R,")
  344. if indexr != -1: # Rectangle, example: %ADD15R,0.05X0.12*%
  345. # Hack explained above
  346. apid = str(int(gline[4:indexr]))
  347. indexx = gline.find("X")
  348. self.apertures[apid] = {"type": "R",
  349. "width": float(gline[indexr+2:indexx]),
  350. "height": float(gline[indexx+1:indexstar])}
  351. return apid
  352. indexo = gline.find("O,")
  353. if indexo != -1: # Obround
  354. # Hack explained above
  355. apid = str(int(gline[4:indexo]))
  356. indexx = gline.find("X")
  357. self.apertures[apid] = {"type": "O",
  358. "width": float(gline[indexo+2:indexx]),
  359. "height": float(gline[indexx+1:indexstar])}
  360. return apid
  361. print "WARNING: Aperture not implemented:", gline
  362. return None
  363. def parse_file(self, filename):
  364. """
  365. Calls Gerber.parse_lines() with array of lines
  366. read from the given file.
  367. """
  368. gfile = open(filename, 'r')
  369. gstr = gfile.readlines()
  370. gfile.close()
  371. self.parse_lines(gstr)
  372. def parse_lines(self, glines):
  373. """
  374. Main Gerber parser. Reads Gerber and populates ``self.paths``, ``self.apertures``,
  375. ``self.flashes``, ``self.regions`` and ``self.units``.
  376. :param glines: Gerber code as list of strings, each
  377. element being one line of the source file.
  378. :type glines: list
  379. :return: None
  380. :rtype: None
  381. """
  382. path = [] # Coordinates of the current path
  383. last_path_aperture = None
  384. current_aperture = None
  385. # 1,2 or 3 from "G01", "G02" or "G03"
  386. current_interpolation_mode = None
  387. # 1 or 2 from "D01" or "D02"
  388. # Note this is to support deprecated Gerber not putting
  389. # an operation code at the end of every coordinate line.
  390. current_operation_code = None
  391. # Current coordinates
  392. current_x = None
  393. current_y = None
  394. for gline in glines:
  395. # Linear interpolation plus flashes
  396. match = self.lin_re.search(gline)
  397. if match:
  398. # Parse coordinates
  399. if match.group(2) is not None:
  400. current_x = parse_gerber_number(match.group(2), self.frac_digits)
  401. if match.group(3) is not None:
  402. current_y = parse_gerber_number(match.group(3), self.frac_digits)
  403. # Parse operation code
  404. if match.group(4) is not None:
  405. current_operation_code = match.group(4)
  406. # Pen down: add segment
  407. if current_operation_code == '1':
  408. path.append([current_x, current_y])
  409. last_path_aperture = current_aperture
  410. # Pen up: finish path
  411. elif current_operation_code == '2':
  412. if len(path) > 1:
  413. self.paths.append({"linestring": LineString(path),
  414. "aperture": last_path_aperture})
  415. path = [[current_x, current_y]]
  416. # Flash
  417. elif current_operation_code == '3':
  418. self.flashes.append({"loc": [current_x, current_y],
  419. "aperture": current_aperture})
  420. continue
  421. # if gline.find("D01*") != -1: # pen down
  422. # path.append(parse_gerber_coords(gline, self.int_digits, self.frac_digits))
  423. # last_path_aperture = current_aperture
  424. # continue
  425. #
  426. # if gline.find("D02*") != -1: # pen up
  427. # if len(path) > 1:
  428. # # Path completed, create shapely LineString
  429. # self.paths.append({"linestring": LineString(path),
  430. # "aperture": last_path_aperture})
  431. # path = [parse_gerber_coords(gline, self.int_digits, self.frac_digits)]
  432. # continue
  433. #
  434. # indexd3 = gline.find("D03*")
  435. # if indexd3 > 0: # Flash
  436. # self.flashes.append({"loc": parse_gerber_coords(gline, self.int_digits, self.frac_digits),
  437. # "aperture": current_aperture})
  438. # continue
  439. # if indexd3 == 0: # Flash?
  440. # print "WARNING: Uninplemented flash style:", gline
  441. # continue
  442. # End region
  443. if self.regionoff_re.search(gline):
  444. # Only one path defines region?
  445. self.regions.append({"polygon": Polygon(path),
  446. "aperture": last_path_aperture})
  447. path = []
  448. continue
  449. # if gline.find("G37*") != -1: # end region
  450. # # Only one path defines region?
  451. # self.regions.append({"polygon": Polygon(path),
  452. # "aperture": last_path_aperture})
  453. # path = []
  454. # continue
  455. if gline.find("%ADD") != -1: # aperture definition
  456. self.aperture_parse(gline) # adds element to apertures
  457. continue
  458. # Interpolation mode change
  459. # Can occur along with coordinates and operation code but
  460. # sometimes by itself (handled here).
  461. # Example: G01*
  462. match = self.interp_re.search(gline)
  463. if match:
  464. current_interpolation_mode = int(match.group(1))
  465. continue
  466. # Tool/aperture change
  467. # Example: D12*
  468. match = self.tool_re.search(gline)
  469. if match:
  470. current_aperture = match.group(1)
  471. continue
  472. # indexstar = gline.find("*")
  473. # if gline.find("D") == 0: # Aperture change
  474. # current_aperture = gline[1:indexstar]
  475. # continue
  476. # if gline.find("G54D") == 0: # Aperture change (deprecated)
  477. # current_aperture = gline[4:indexstar]
  478. # continue
  479. # Number format
  480. # TODO: This is ignoring most of the format. Implement the rest.
  481. match = self.fmt_re.search(gline)
  482. if match:
  483. self.int_digits = int(match.group(3))
  484. self.frac_digits = int(match.group(4))
  485. continue
  486. # if gline.find("%FS") != -1: # Format statement
  487. # indexx = gline.find("X")
  488. # self.int_digits = int(gline[indexx + 1])
  489. # self.frac_digits = int(gline[indexx + 2])
  490. # continue
  491. # Mode (IN/MM)
  492. match = self.mode_re.search(gline)
  493. if match:
  494. self.units = match.group(1)
  495. continue
  496. print "WARNING: Line ignored:", gline
  497. if len(path) > 1:
  498. # EOF, create shapely LineString if something still in path
  499. self.paths.append({"linestring": LineString(path),
  500. "aperture": last_path_aperture})
  501. # if len(path) > 1:
  502. # # EOF, create shapely LineString if something still in path
  503. # self.paths.append({"linestring": LineString(path),
  504. # "aperture": current_aperture})
  505. def do_flashes(self):
  506. """
  507. Creates geometry for Gerber flashes (aperture on a single point).
  508. """
  509. self.flash_geometry = []
  510. for flash in self.flashes:
  511. aperture = self.apertures[flash['aperture']]
  512. if aperture['type'] == 'C': # Circles
  513. circle = Point(flash['loc']).buffer(aperture['size']/2)
  514. self.flash_geometry.append(circle)
  515. continue
  516. if aperture['type'] == 'R': # Rectangles
  517. loc = flash['loc']
  518. width = aperture['width']
  519. height = aperture['height']
  520. minx = loc[0] - width/2
  521. maxx = loc[0] + width/2
  522. miny = loc[1] - height/2
  523. maxy = loc[1] + height/2
  524. rectangle = shply_box(minx, miny, maxx, maxy)
  525. self.flash_geometry.append(rectangle)
  526. continue
  527. if aperture['type'] == 'O': # Obround
  528. loc = flash['loc']
  529. width = aperture['width']
  530. height = aperture['height']
  531. if width > height:
  532. p1 = Point(loc[0] + 0.5*(width-height), loc[1])
  533. p2 = Point(loc[0] - 0.5*(width-height), loc[1])
  534. c1 = p1.buffer(height*0.5)
  535. c2 = p2.buffer(height*0.5)
  536. else:
  537. p1 = Point(loc[0], loc[1] + 0.5*(height-width))
  538. p2 = Point(loc[0], loc[1] - 0.5*(height-width))
  539. c1 = p1.buffer(width*0.5)
  540. c2 = p2.buffer(width*0.5)
  541. obround = cascaded_union([c1, c2]).convex_hull
  542. self.flash_geometry.append(obround)
  543. continue
  544. print "WARNING: Aperture type %s not implemented" % (aperture['type'])
  545. def create_geometry(self):
  546. """
  547. Geometry from a Gerber file is made up entirely of polygons.
  548. Every stroke (linear or circular) has an aperture which gives
  549. it thickness. Additionally, aperture strokes have non-zero area,
  550. and regions naturally do as well.
  551. :rtype : None
  552. :return: None
  553. """
  554. # if len(self.buffered_paths) == 0:
  555. # self.buffer_paths()
  556. print "... buffer_paths()"
  557. self.buffer_paths()
  558. print "... fix_regions()"
  559. self.fix_regions()
  560. print "... do_flashes()"
  561. self.do_flashes()
  562. print "... cascaded_union()"
  563. self.solid_geometry = cascaded_union(
  564. self.buffered_paths +
  565. [poly['polygon'] for poly in self.regions] +
  566. self.flash_geometry)
  567. def get_bounding_box(self, margin=0.0, rounded=False):
  568. """
  569. Creates and returns a rectangular polygon bounding at a distance of
  570. margin from the object's ``solid_geometry``. If margin > 0, the polygon
  571. can optionally have rounded corners of radius equal to margin.
  572. :param margin: Distance to enlarge the rectangular bounding
  573. box in both positive and negative, x and y axes.
  574. :type margin: float
  575. :param rounded: Wether or not to have rounded corners.
  576. :type rounded: bool
  577. :return: The bounding box.
  578. :rtype: Shapely.Polygon
  579. """
  580. bbox = self.solid_geometry.envelope.buffer(margin)
  581. if not rounded:
  582. bbox = bbox.envelope
  583. return bbox
  584. class Excellon(Geometry):
  585. """
  586. *ATTRIBUTES*
  587. * ``tools`` (dict): The key is the tool name and the value is
  588. the size (diameter).
  589. * ``drills`` (list): Each is a dictionary:
  590. ================ ====================================
  591. Key Value
  592. ================ ====================================
  593. point (Shapely.Point) Where to drill
  594. tool (str) A key in ``tools``
  595. ================ ====================================
  596. """
  597. def __init__(self):
  598. """
  599. The constructor takes no parameters.
  600. :return: Excellon object.
  601. :rtype: Excellon
  602. """
  603. Geometry.__init__(self)
  604. self.tools = {}
  605. self.drills = []
  606. # Trailing "T" or leading "L"
  607. self.zeros = ""
  608. # Attributes to be included in serialization
  609. # Always append to it because it carries contents
  610. # from Geometry.
  611. self.ser_attrs += ['tools', 'drills', 'zeros']
  612. def parse_file(self, filename):
  613. efile = open(filename, 'r')
  614. estr = efile.readlines()
  615. efile.close()
  616. self.parse_lines(estr)
  617. def parse_lines(self, elines):
  618. """
  619. Main Excellon parser.
  620. """
  621. units_re = re.compile(r'^(INCH|METRIC)(?:,([TL])Z)?$')
  622. current_tool = ""
  623. for eline in elines:
  624. ## Tool definitions ##
  625. # TODO: Verify all this
  626. indexT = eline.find("T")
  627. indexC = eline.find("C")
  628. indexF = eline.find("F")
  629. # Type 1
  630. if indexT != -1 and indexC > indexT and indexF > indexC:
  631. tool = eline[1:indexC]
  632. spec = eline[indexC+1:indexF]
  633. self.tools[tool] = float(spec)
  634. continue
  635. # Type 2
  636. # TODO: Is this inches?
  637. #indexsp = eline.find(" ")
  638. #indexin = eline.find("in")
  639. #if indexT != -1 and indexsp > indexT and indexin > indexsp:
  640. # tool = eline[1:indexsp]
  641. # spec = eline[indexsp+1:indexin]
  642. # self.tools[tool] = spec
  643. # continue
  644. # Type 3
  645. if indexT != -1 and indexC > indexT:
  646. tool = eline[1:indexC]
  647. spec = eline[indexC+1:-1]
  648. self.tools[tool] = float(spec)
  649. continue
  650. ## Tool change
  651. if indexT == 0:
  652. current_tool = eline[1:-1]
  653. continue
  654. ## Drill
  655. indexx = eline.find("X")
  656. indexy = eline.find("Y")
  657. if indexx != -1 and indexy != -1:
  658. x = float(int(eline[indexx+1:indexy])/10000.0)
  659. y = float(int(eline[indexy+1:-1])/10000.0)
  660. self.drills.append({'point': Point((x, y)), 'tool': current_tool})
  661. continue
  662. # Units and number format
  663. match = units_re.match(eline)
  664. if match:
  665. self.zeros = match.group(2) # "T" or "L"
  666. self.units = {"INCH": "IN", "METRIC": "MM"}[match.group(1)]
  667. print "WARNING: Line ignored:", eline
  668. def create_geometry(self):
  669. self.solid_geometry = []
  670. sizes = {}
  671. for tool in self.tools:
  672. sizes[tool] = float(self.tools[tool])
  673. for drill in self.drills:
  674. poly = Point(drill['point']).buffer(sizes[drill['tool']]/2.0)
  675. self.solid_geometry.append(poly)
  676. self.solid_geometry = cascaded_union(self.solid_geometry)
  677. def scale(self, factor):
  678. """
  679. Scales geometry on the XY plane in the object by a given factor.
  680. Tool sizes, feedrates an Z-plane dimensions are untouched.
  681. :param factor: Number by which to scale the object.
  682. :type factor: float
  683. :return: None
  684. :rtype: NOne
  685. """
  686. # Drills
  687. for drill in self.drills:
  688. drill['point'] = affinity.scale(drill['point'], factor, factor, origin=(0, 0))
  689. def convert_units(self, units):
  690. factor = Geometry.convert_units(self, units)
  691. # Tools
  692. for tname in self.tools:
  693. self.tools[tname] *= factor
  694. return factor
  695. class CNCjob(Geometry):
  696. """
  697. Represents work to be done by a CNC machine.
  698. *ATTRIBUTES*
  699. * ``gcode_parsed`` (list): Each is a dictionary:
  700. ===================== =========================================
  701. Key Value
  702. ===================== =========================================
  703. geom (Shapely.LineString) Tool path (XY plane)
  704. kind (string) "AB", A is "T" (travel) or
  705. "C" (cut). B is "F" (fast) or "S" (slow).
  706. ===================== =========================================
  707. """
  708. def __init__(self, units="in", kind="generic", z_move=0.1,
  709. feedrate=3.0, z_cut=-0.002, tooldia=0.0):
  710. Geometry.__init__(self)
  711. self.kind = kind
  712. self.units = units
  713. self.z_cut = z_cut
  714. self.z_move = z_move
  715. self.feedrate = feedrate
  716. self.tooldia = tooldia
  717. self.unitcode = {"IN": "G20", "MM": "G21"}
  718. self.pausecode = "G04 P1"
  719. self.feedminutecode = "G94"
  720. self.absolutecode = "G90"
  721. self.gcode = ""
  722. self.input_geometry_bounds = None
  723. self.gcode_parsed = None
  724. self.steps_per_circ = 20 # Used when parsing G-code arcs
  725. # Attributes to be included in serialization
  726. # Always append to it because it carries contents
  727. # from Geometry.
  728. self.ser_attrs += ['kind', 'z_cut', 'z_move', 'feedrate', 'tooldia',
  729. 'gcode', 'input_geometry_bounds', 'gcode_parsed',
  730. 'steps_per_circ']
  731. def convert_units(self, units):
  732. factor = Geometry.convert_units(self, units)
  733. print "CNCjob.convert_units()"
  734. self.z_cut *= factor
  735. self.z_move *= factor
  736. self.feedrate *= factor
  737. self.tooldia *= factor
  738. return factor
  739. def generate_from_excellon(self, exobj):
  740. """
  741. Generates G-code for drilling from Excellon object.
  742. self.gcode becomes a list, each element is a
  743. different job for each tool in the excellon code.
  744. """
  745. self.kind = "drill"
  746. self.gcode = []
  747. t = "G00 X%.4fY%.4f\n"
  748. down = "G01 Z%.4f\n" % self.z_cut
  749. up = "G01 Z%.4f\n" % self.z_move
  750. for tool in exobj.tools:
  751. points = []
  752. for drill in exobj.drill:
  753. if drill['tool'] == tool:
  754. points.append(drill['point'])
  755. gcode = self.unitcode[self.units.upper()] + "\n"
  756. gcode += self.absolutecode + "\n"
  757. gcode += self.feedminutecode + "\n"
  758. gcode += "F%.2f\n" % self.feedrate
  759. gcode += "G00 Z%.4f\n" % self.z_move # Move to travel height
  760. gcode += "M03\n" # Spindle start
  761. gcode += self.pausecode + "\n"
  762. for point in points:
  763. gcode += t % point
  764. gcode += down + up
  765. gcode += t % (0, 0)
  766. gcode += "M05\n" # Spindle stop
  767. self.gcode.append(gcode)
  768. def generate_from_excellon_by_tool(self, exobj, tools="all"):
  769. """
  770. Creates gcode for this object from an Excellon object
  771. for the specified tools.
  772. :param exobj: Excellon object to process
  773. :type exobj: Excellon
  774. :param tools: Comma separated tool names
  775. :type: tools: str
  776. :return: None
  777. :rtype: None
  778. """
  779. print "Creating CNC Job from Excellon..."
  780. if tools == "all":
  781. tools = [tool for tool in exobj.tools]
  782. else:
  783. tools = [x.strip() for x in tools.split(",")]
  784. tools = filter(lambda y: y in exobj.tools, tools)
  785. print "Tools are:", tools
  786. points = []
  787. for drill in exobj.drills:
  788. if drill['tool'] in tools:
  789. points.append(drill['point'])
  790. print "Found %d drills." % len(points)
  791. #self.kind = "drill"
  792. self.gcode = []
  793. t = "G00 X%.4fY%.4f\n"
  794. down = "G01 Z%.4f\n" % self.z_cut
  795. up = "G01 Z%.4f\n" % self.z_move
  796. gcode = self.unitcode[self.units.upper()] + "\n"
  797. gcode += self.absolutecode + "\n"
  798. gcode += self.feedminutecode + "\n"
  799. gcode += "F%.2f\n" % self.feedrate
  800. gcode += "G00 Z%.4f\n" % self.z_move # Move to travel height
  801. gcode += "M03\n" # Spindle start
  802. gcode += self.pausecode + "\n"
  803. for point in points:
  804. x, y = point.coords.xy
  805. gcode += t % (x[0], y[0])
  806. gcode += down + up
  807. gcode += t % (0, 0)
  808. gcode += "M05\n" # Spindle stop
  809. self.gcode = gcode
  810. def generate_from_geometry(self, geometry, append=True, tooldia=None, tolerance=0):
  811. """
  812. Generates G-Code from a Geometry object. Stores in ``self.gcode``.
  813. :param geometry: Geometry defining the toolpath
  814. :type geometry: Geometry
  815. :param append: Wether to append to self.gcode or re-write it.
  816. :type append: bool
  817. :param tooldia: If given, sets the tooldia property but does
  818. not affect the process in any other way.
  819. :type tooldia: bool
  820. :param tolerance: All points in the simplified object will be within the
  821. tolerance distance of the original geometry.
  822. :return: None
  823. :rtype: None
  824. """
  825. if tooldia is not None:
  826. self.tooldia = tooldia
  827. self.input_geometry_bounds = geometry.bounds()
  828. if not append:
  829. self.gcode = ""
  830. self.gcode = self.unitcode[self.units.upper()] + "\n"
  831. self.gcode += self.absolutecode + "\n"
  832. self.gcode += self.feedminutecode + "\n"
  833. self.gcode += "F%.2f\n" % self.feedrate
  834. self.gcode += "G00 Z%.4f\n" % self.z_move # Move to travel height
  835. self.gcode += "M03\n" # Spindle start
  836. self.gcode += self.pausecode + "\n"
  837. for geo in geometry.solid_geometry:
  838. if type(geo) == Polygon:
  839. self.gcode += self.polygon2gcode(geo, tolerance=tolerance)
  840. continue
  841. if type(geo) == LineString or type(geo) == LinearRing:
  842. self.gcode += self.linear2gcode(geo, tolerance=tolerance)
  843. continue
  844. if type(geo) == Point:
  845. self.gcode += self.point2gcode(geo)
  846. continue
  847. if type(geo) == MultiPolygon:
  848. for poly in geo:
  849. self.gcode += self.polygon2gcode(poly, tolerance=tolerance)
  850. continue
  851. print "WARNING: G-code generation not implemented for %s" % (str(type(geo)))
  852. self.gcode += "G00 Z%.4f\n" % self.z_move # Stop cutting
  853. self.gcode += "G00 X0Y0\n"
  854. self.gcode += "M05\n" # Spindle stop
  855. def pre_parse(self, gtext):
  856. """
  857. Separates parts of the G-Code text into a list of dictionaries.
  858. Used by ``self.gcode_parse()``.
  859. :param gtext: A single string with g-code
  860. """
  861. # Units: G20-inches, G21-mm
  862. units_re = re.compile(r'^G2([01])')
  863. # TODO: This has to be re-done
  864. gcmds = []
  865. lines = gtext.split("\n") # TODO: This is probably a lot of work!
  866. for line in lines:
  867. # Clean up
  868. line = line.strip()
  869. # Remove comments
  870. # NOTE: Limited to 1 bracket pair
  871. op = line.find("(")
  872. cl = line.find(")")
  873. if op > -1 and cl > op:
  874. #comment = line[op+1:cl]
  875. line = line[:op] + line[(cl+1):]
  876. # Units
  877. match = units_re.match(line)
  878. if match:
  879. self.units = {'0': "IN", '1': "MM"}[match.group(1)]
  880. # Parse GCode
  881. # 0 4 12
  882. # G01 X-0.007 Y-0.057
  883. # --> codes_idx = [0, 4, 12]
  884. codes = "NMGXYZIJFP"
  885. codes_idx = []
  886. i = 0
  887. for ch in line:
  888. if ch in codes:
  889. codes_idx.append(i)
  890. i += 1
  891. n_codes = len(codes_idx)
  892. if n_codes == 0:
  893. continue
  894. # Separate codes in line
  895. parts = []
  896. for p in range(n_codes-1):
  897. parts.append(line[codes_idx[p]:codes_idx[p+1]].strip())
  898. parts.append(line[codes_idx[-1]:].strip())
  899. # Separate codes from values
  900. cmds = {}
  901. for part in parts:
  902. cmds[part[0]] = float(part[1:])
  903. gcmds.append(cmds)
  904. return gcmds
  905. def gcode_parse(self):
  906. """
  907. G-Code parser (from self.gcode). Generates dictionary with
  908. single-segment LineString's and "kind" indicating cut or travel,
  909. fast or feedrate speed.
  910. """
  911. # Results go here
  912. geometry = []
  913. # TODO: Merge into single parser?
  914. gobjs = self.pre_parse(self.gcode)
  915. # Last known instruction
  916. current = {'X': 0.0, 'Y': 0.0, 'Z': 0.0, 'G': 0}
  917. # Process every instruction
  918. for gobj in gobjs:
  919. if 'Z' in gobj:
  920. if ('X' in gobj or 'Y' in gobj) and gobj['Z'] != current['Z']:
  921. print "WARNING: Non-orthogonal motion: From", current
  922. print " To:", gobj
  923. current['Z'] = gobj['Z']
  924. if 'G' in gobj:
  925. current['G'] = int(gobj['G'])
  926. if 'X' in gobj or 'Y' in gobj:
  927. x = 0
  928. y = 0
  929. kind = ["C", "F"] # T=travel, C=cut, F=fast, S=slow
  930. if 'X' in gobj:
  931. x = gobj['X']
  932. else:
  933. x = current['X']
  934. if 'Y' in gobj:
  935. y = gobj['Y']
  936. else:
  937. y = current['Y']
  938. if current['Z'] > 0:
  939. kind[0] = 'T'
  940. if current['G'] > 0:
  941. kind[1] = 'S'
  942. arcdir = [None, None, "cw", "ccw"]
  943. if current['G'] in [0, 1]: # line
  944. geometry.append({'geom': LineString([(current['X'], current['Y']),
  945. (x, y)]), 'kind': kind})
  946. if current['G'] in [2, 3]: # arc
  947. center = [gobj['I'] + current['X'], gobj['J'] + current['Y']]
  948. radius = sqrt(gobj['I']**2 + gobj['J']**2)
  949. start = arctan2(-gobj['J'], -gobj['I'])
  950. stop = arctan2(-center[1]+y, -center[0]+x)
  951. geometry.append({'geom': arc(center, radius, start, stop,
  952. arcdir[current['G']],
  953. self.steps_per_circ),
  954. 'kind': kind})
  955. # Update current instruction
  956. for code in gobj:
  957. current[code] = gobj[code]
  958. #self.G_geometry = geometry
  959. self.gcode_parsed = geometry
  960. return geometry
  961. # def plot(self, tooldia=None, dpi=75, margin=0.1,
  962. # color={"T": ["#F0E24D", "#B5AB3A"], "C": ["#5E6CFF", "#4650BD"]},
  963. # alpha={"T": 0.3, "C": 1.0}):
  964. # """
  965. # Creates a Matplotlib figure with a plot of the
  966. # G-code job.
  967. # """
  968. # if tooldia is None:
  969. # tooldia = self.tooldia
  970. #
  971. # fig = Figure(dpi=dpi)
  972. # ax = fig.add_subplot(111)
  973. # ax.set_aspect(1)
  974. # xmin, ymin, xmax, ymax = self.input_geometry_bounds
  975. # ax.set_xlim(xmin-margin, xmax+margin)
  976. # ax.set_ylim(ymin-margin, ymax+margin)
  977. #
  978. # if tooldia == 0:
  979. # for geo in self.gcode_parsed:
  980. # linespec = '--'
  981. # linecolor = color[geo['kind'][0]][1]
  982. # if geo['kind'][0] == 'C':
  983. # linespec = 'k-'
  984. # x, y = geo['geom'].coords.xy
  985. # ax.plot(x, y, linespec, color=linecolor)
  986. # else:
  987. # for geo in self.gcode_parsed:
  988. # poly = geo['geom'].buffer(tooldia/2.0)
  989. # patch = PolygonPatch(poly, facecolor=color[geo['kind'][0]][0],
  990. # edgecolor=color[geo['kind'][0]][1],
  991. # alpha=alpha[geo['kind'][0]], zorder=2)
  992. # ax.add_patch(patch)
  993. #
  994. # return fig
  995. def plot2(self, axes, tooldia=None, dpi=75, margin=0.1,
  996. color={"T": ["#F0E24D", "#B5AB3A"], "C": ["#5E6CFF", "#4650BD"]},
  997. alpha={"T": 0.3, "C": 1.0}, tool_tolerance=0.001):
  998. """
  999. Plots the G-code job onto the given axes.
  1000. :param axes: Matplotlib axes on which to plot.
  1001. :param tooldia: Tool diameter.
  1002. :param dpi: Not used!
  1003. :param margin: Not used!
  1004. :param color: Color specification.
  1005. :param alpha: Transparency specification.
  1006. :param tool_tolerance: Tolerance when drawing the toolshape.
  1007. :return: None
  1008. """
  1009. if tooldia is None:
  1010. tooldia = self.tooldia
  1011. if tooldia == 0:
  1012. for geo in self.gcode_parsed:
  1013. linespec = '--'
  1014. linecolor = color[geo['kind'][0]][1]
  1015. if geo['kind'][0] == 'C':
  1016. linespec = 'k-'
  1017. x, y = geo['geom'].coords.xy
  1018. axes.plot(x, y, linespec, color=linecolor)
  1019. else:
  1020. for geo in self.gcode_parsed:
  1021. poly = geo['geom'].buffer(tooldia/2.0).simplify(tool_tolerance)
  1022. patch = PolygonPatch(poly, facecolor=color[geo['kind'][0]][0],
  1023. edgecolor=color[geo['kind'][0]][1],
  1024. alpha=alpha[geo['kind'][0]], zorder=2)
  1025. axes.add_patch(patch)
  1026. def create_geometry(self):
  1027. # TODO: This takes forever. Too much data?
  1028. self.solid_geometry = cascaded_union([geo['geom'] for geo in self.gcode_parsed])
  1029. def polygon2gcode(self, polygon, tolerance=0):
  1030. """
  1031. Creates G-Code for the exterior and all interior paths
  1032. of a polygon.
  1033. :param polygon: A Shapely.Polygon
  1034. :type polygon: Shapely.Polygon
  1035. :param tolerance: All points in the simplified object will be within the
  1036. tolerance distance of the original geometry.
  1037. :type tolerance: float
  1038. :return: G-code to cut along polygon.
  1039. :rtype: str
  1040. """
  1041. if tolerance > 0:
  1042. target_polygon = polygon.simplify(tolerance)
  1043. else:
  1044. target_polygon = polygon
  1045. gcode = ""
  1046. t = "G0%d X%.4fY%.4f\n"
  1047. path = list(target_polygon.exterior.coords) # Polygon exterior
  1048. gcode += t % (0, path[0][0], path[0][1]) # Move to first point
  1049. gcode += "G01 Z%.4f\n" % self.z_cut # Start cutting
  1050. for pt in path[1:]:
  1051. gcode += t % (1, pt[0], pt[1]) # Linear motion to point
  1052. gcode += "G00 Z%.4f\n" % self.z_move # Stop cutting
  1053. for ints in target_polygon.interiors: # Polygon interiors
  1054. path = list(ints.coords)
  1055. gcode += t % (0, path[0][0], path[0][1]) # Move to first point
  1056. gcode += "G01 Z%.4f\n" % self.z_cut # Start cutting
  1057. for pt in path[1:]:
  1058. gcode += t % (1, pt[0], pt[1]) # Linear motion to point
  1059. gcode += "G00 Z%.4f\n" % self.z_move # Stop cutting
  1060. return gcode
  1061. def linear2gcode(self, linear, tolerance=0):
  1062. """
  1063. Generates G-code to cut along the linear feature.
  1064. :param linear: The path to cut along.
  1065. :type: Shapely.LinearRing or Shapely.Linear String
  1066. :param tolerance: All points in the simplified object will be within the
  1067. tolerance distance of the original geometry.
  1068. :type tolerance: float
  1069. :return: G-code to cut alon the linear feature.
  1070. :rtype: str
  1071. """
  1072. if tolerance > 0:
  1073. target_linear = linear.simplify(tolerance)
  1074. else:
  1075. target_linear = linear
  1076. gcode = ""
  1077. t = "G0%d X%.4fY%.4f\n"
  1078. path = list(target_linear.coords)
  1079. gcode += t % (0, path[0][0], path[0][1]) # Move to first point
  1080. gcode += "G01 Z%.4f\n" % self.z_cut # Start cutting
  1081. for pt in path[1:]:
  1082. gcode += t % (1, pt[0], pt[1]) # Linear motion to point
  1083. gcode += "G00 Z%.4f\n" % self.z_move # Stop cutting
  1084. return gcode
  1085. def point2gcode(self, point):
  1086. # TODO: This is not doing anything.
  1087. gcode = ""
  1088. t = "G0%d X%.4fY%.4f\n"
  1089. path = list(point.coords)
  1090. gcode += t % (0, path[0][0], path[0][1]) # Move to first point
  1091. gcode += "G01 Z%.4f\n" % self.z_cut # Start cutting
  1092. gcode += "G00 Z%.4f\n" % self.z_move # Stop cutting
  1093. def scale(self, factor):
  1094. """
  1095. Scales all the geometry on the XY plane in the object by the
  1096. given factor. Tool sizes, feedrates, or Z-axis dimensions are
  1097. not altered.
  1098. :param factor: Number by which to scale the object.
  1099. :type factor: float
  1100. :return: None
  1101. :rtype: None
  1102. """
  1103. for g in self.gcode_parsed:
  1104. g['geom'] = affinity.scale(g['geom'], factor, factor, origin=(0, 0))
  1105. self.create_geometry()
  1106. def get_bounds(geometry_set):
  1107. xmin = Inf
  1108. ymin = Inf
  1109. xmax = -Inf
  1110. ymax = -Inf
  1111. print "Getting bounds of:", str(geometry_set)
  1112. for gs in geometry_set:
  1113. try:
  1114. gxmin, gymin, gxmax, gymax = geometry_set[gs].bounds()
  1115. xmin = min([xmin, gxmin])
  1116. ymin = min([ymin, gymin])
  1117. xmax = max([xmax, gxmax])
  1118. ymax = max([ymax, gymax])
  1119. except:
  1120. print "DEV WARNING: Tried to get bounds of empty geometry."
  1121. return [xmin, ymin, xmax, ymax]
  1122. def arc(center, radius, start, stop, direction, steps_per_circ):
  1123. """
  1124. Creates a Shapely.LineString for the specified arc.
  1125. :param center: Coordinates of the center [x, y]
  1126. :type center: list
  1127. :param radius: Radius of the arc.
  1128. :type radius: float
  1129. :param start: Starting angle in radians
  1130. :type start: float
  1131. :param stop: End angle in radians
  1132. :type stop: float
  1133. :param direction: Orientation of the arc, "CW" or "CCW"
  1134. :type direction: string
  1135. :param steps_per_circ: Number of straight line segments to
  1136. represent a circle.
  1137. :type steps_per_circ: int
  1138. :return: The desired arc.
  1139. :rtype: Shapely.LineString
  1140. """
  1141. da_sign = {"cw": -1.0, "ccw": 1.0}
  1142. points = []
  1143. if direction == "ccw" and stop <= start:
  1144. stop += 2*pi
  1145. if direction == "cw" and stop >= start:
  1146. stop -= 2*pi
  1147. angle = abs(stop - start)
  1148. #angle = stop-start
  1149. steps = max([int(ceil(angle/(2*pi)*steps_per_circ)), 2])
  1150. delta_angle = da_sign[direction]*angle*1.0/steps
  1151. for i in range(steps+1):
  1152. theta = start + delta_angle*i
  1153. points.append([center[0]+radius*cos(theta), center[1]+radius*sin(theta)])
  1154. return LineString(points)
  1155. def clear_poly(poly, tooldia, overlap=0.1):
  1156. """
  1157. Creates a list of Shapely geometry objects covering the inside
  1158. of a Shapely.Polygon. Use for removing all the copper in a region
  1159. or bed flattening.
  1160. :param poly: Target polygon
  1161. :type poly: Shapely.Polygon
  1162. :param tooldia: Diameter of the tool
  1163. :type tooldia: float
  1164. :param overlap: Fraction of the tool diameter to overlap
  1165. in each pass.
  1166. :type overlap: float
  1167. :return: list of Shapely.Polygon
  1168. :rtype: list
  1169. """
  1170. poly_cuts = [poly.buffer(-tooldia/2.0)]
  1171. while True:
  1172. poly = poly_cuts[-1].buffer(-tooldia*(1-overlap))
  1173. if poly.area > 0:
  1174. poly_cuts.append(poly)
  1175. else:
  1176. break
  1177. return poly_cuts
  1178. def find_polygon(poly_set, point):
  1179. """
  1180. Return the first polygon in the list of polygons poly_set
  1181. that contains the given point.
  1182. """
  1183. p = Point(point)
  1184. for poly in poly_set:
  1185. if poly.contains(p):
  1186. return poly
  1187. return None
  1188. def to_dict(geo):
  1189. output = ''
  1190. if isinstance(geo, BaseGeometry):
  1191. return {
  1192. "__class__": "Shply",
  1193. "__inst__": sdumps(geo)
  1194. }
  1195. return geo
  1196. def dict2obj(d):
  1197. if '__class__' in d and '__inst__' in d:
  1198. # For now assume all classes are Shapely geometry.
  1199. return sloads(d['__inst__'])
  1200. else:
  1201. return d
  1202. def plotg(geo):
  1203. try:
  1204. _ = iter(geo)
  1205. except:
  1206. geo = [geo]
  1207. for g in geo:
  1208. if type(g) == Polygon:
  1209. x, y = g.exterior.coords.xy
  1210. plot(x, y)
  1211. for ints in g.interiors:
  1212. x, y = ints.coords.xy
  1213. plot(x, y)
  1214. continue
  1215. if type(g) == LineString or type(g) == LinearRing:
  1216. x, y = g.coords.xy
  1217. plot(x, y)
  1218. continue
  1219. if type(g) == Point:
  1220. x, y = g.coords.xy
  1221. plot(x, y, 'o')
  1222. continue
  1223. try:
  1224. _ = iter(g)
  1225. plotg(g)
  1226. except:
  1227. print "Cannot plot:", str(type(g))
  1228. continue
  1229. def parse_gerber_number(strnumber, frac_digits):
  1230. """
  1231. Parse a single number of Gerber coordinates.
  1232. :param strnumber: String containing a number in decimal digits
  1233. from a coordinate data block, possibly with a leading sign.
  1234. :type strnumber: str
  1235. :param frac_digits: Number of digits used for the fractional
  1236. part of the number
  1237. :type frac_digits: int
  1238. :return: The number in floating point.
  1239. :rtype: float
  1240. """
  1241. return int(strnumber)*(10**(-frac_digits))
  1242. def parse_gerber_coords(gstr, int_digits, frac_digits):
  1243. """
  1244. Parse Gerber coordinates
  1245. :param gstr: Line of G-Code containing coordinates.
  1246. :type gstr: str
  1247. :param int_digits: Number of digits in integer part of a number.
  1248. :type int_digits: int
  1249. :param frac_digits: Number of digits in frac_digits part of a number.
  1250. :type frac_digits: int
  1251. :return: [x, y] coordinates.
  1252. :rtype: list
  1253. """
  1254. global gerbx, gerby
  1255. xindex = gstr.find("X")
  1256. yindex = gstr.find("Y")
  1257. index = gstr.find("D")
  1258. if xindex == -1:
  1259. x = gerbx
  1260. y = int(gstr[(yindex+1):index])*(10**(-frac_digits))
  1261. elif yindex == -1:
  1262. y = gerby
  1263. x = int(gstr[(xindex+1):index])*(10**(-frac_digits))
  1264. else:
  1265. x = int(gstr[(xindex+1):yindex])*(10**(-frac_digits))
  1266. y = int(gstr[(yindex+1):index])*(10**(-frac_digits))
  1267. gerbx = x
  1268. gerby = y
  1269. return [x, y]