From ab69ee0172353e64fbe5099a974341e88feaf24b Mon Sep 17 00:00:00 2001 From: Paulo Henrique Silva Date: Mon, 10 Nov 2014 12:24:09 -0200 Subject: Bunch of small fixes to improve Gerber read/write. --- gerber/utils.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) (limited to 'gerber/utils.py') diff --git a/gerber/utils.py b/gerber/utils.py index 7749e22..56b675f 100644 --- a/gerber/utils.py +++ b/gerber/utils.py @@ -125,9 +125,9 @@ def write_gerber_value(value, format=(2, 5), zero_suppression='trailing'): if MAX_DIGITS > 13 or integer_digits > 6 or decimal_digits > 7: raise ValueError('Parser only supports precision up to 6:7 format') - # Edge case... + # Edge case... (per Gerber spec we should return 0 in all cases, see page 77) if value == 0: - return '00' + return '0' # negative sign affects padding, so deal with it at the end... negative = value < 0.0 @@ -173,10 +173,14 @@ def decimal_string(value, precision=6, padding=False): integer, decimal = floatstr.split('.') elif ',' in floatstr: integer, decimal = floatstr.split(',') + else: + integer, decimal = floatstr, "0" + if len(decimal) > precision: decimal = decimal[:precision] elif padding: decimal = decimal + (precision - len(decimal)) * '0' + if integer or decimal: return ''.join([integer, '.', decimal]) else: -- cgit From 4bb2e5f8a047d10dafcbc9f841571ac753a439da Mon Sep 17 00:00:00 2001 From: Paulo Henrique Silva Date: Mon, 15 Dec 2014 23:35:01 -0200 Subject: Fix parsing for very short (less 20 lines) files. --- gerber/utils.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) (limited to 'gerber/utils.py') diff --git a/gerber/utils.py b/gerber/utils.py index 56b675f..0f0c07c 100644 --- a/gerber/utils.py +++ b/gerber/utils.py @@ -201,9 +201,14 @@ def detect_file_format(filename): File format. either 'excellon' or 'rs274x' """ - # Read the first 20 lines + # Read the first 20 lines (if possible) + lines = [] with open(filename, 'r') as f: - lines = [next(f) for x in xrange(20)] + try: + for i in range(20): + lines.append(f.readline()) + except StopIteration: + pass # Look for for line in lines: -- cgit From ac89a3c36505bebff68305eb8e315482cba860fd Mon Sep 17 00:00:00 2001 From: Paulo Henrique Silva Date: Wed, 14 Jan 2015 14:31:03 -0200 Subject: Fix for cases whee the coordinate precision is decreased. If we parse a file with 5.5 INCH format and ask to write it back as 2.4 INCH we are going to loose precision and write_gerber_value was not handling these cases write. --- gerber/utils.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) (limited to 'gerber/utils.py') diff --git a/gerber/utils.py b/gerber/utils.py index 0f0c07c..64cd6ed 100644 --- a/gerber/utils.py +++ b/gerber/utils.py @@ -140,12 +140,15 @@ def write_gerber_value(value, format=(2, 5), zero_suppression='trailing'): # Suppression... if zero_suppression == 'trailing': - while digits[-1] == '0': + while digits and digits[-1] == '0': digits.pop() else: - while digits[0] == '0': + while digits and digits[0] == '0': digits.pop(0) + if not digits: + return '0' + return ''.join(digits) if not negative else ''.join(['-'] + digits) -- cgit From 1cc20b351c10b1fa19817f29edd8c54a27aeee4b Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Mon, 2 Feb 2015 11:42:47 -0500 Subject: tests --- gerber/utils.py | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'gerber/utils.py') diff --git a/gerber/utils.py b/gerber/utils.py index 64cd6ed..86119ba 100644 --- a/gerber/utils.py +++ b/gerber/utils.py @@ -220,3 +220,13 @@ def detect_file_format(filename): elif '%FS' in line: return'rs274x' return 'unknown' + + +def validate_coordinates(position): + if position is not None: + if len(position) != 2: + raise TypeError('Position must be a tuple (n=2) of coordinates') + else: + for coord in position: + if not (isinstance(coord, int) or isinstance(coord, float)): + raise TypeError('Coordinates must be integers or floats') -- cgit From aea1f38597824085739aeed1fa6f33e264e23a4b Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Sun, 8 Feb 2015 22:27:24 -0500 Subject: Fix write_gerber_value bug --- gerber/utils.py | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'gerber/utils.py') diff --git a/gerber/utils.py b/gerber/utils.py index 86119ba..23575b3 100644 --- a/gerber/utils.py +++ b/gerber/utils.py @@ -138,6 +138,11 @@ def write_gerber_value(value, format=(2, 5), zero_suppression='trailing'): fmtstring = '%%0%d.0%df' % (MAX_DIGITS + 1, decimal_digits) digits = [val for val in fmtstring % value if val != '.'] + # If all the digits are 0, return '0'. + digit_sum = reduce(lambda x,y:x+int(y), digits, 0) + if digit_sum == 0: + return '0' + # Suppression... if zero_suppression == 'trailing': while digits and digits[-1] == '0': -- cgit From 288ac27084b47166ac662402ea340d0aa25d8f56 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Wed, 18 Feb 2015 04:31:23 -0500 Subject: Get unit conversion working for Gerber/Excellon files Started operations module for file operations/transforms --- gerber/utils.py | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'gerber/utils.py') diff --git a/gerber/utils.py b/gerber/utils.py index 23575b3..542611d 100644 --- a/gerber/utils.py +++ b/gerber/utils.py @@ -26,6 +26,7 @@ files. # Author: Hamilton Kibbe # License: +MILLIMETERS_PER_INCH = 25.4 def parse_gerber_value(value, format=(2, 5), zero_suppression='trailing'): """ Convert gerber/excellon formatted string to floating-point number @@ -235,3 +236,10 @@ def validate_coordinates(position): for coord in position: if not (isinstance(coord, int) or isinstance(coord, float)): raise TypeError('Coordinates must be integers or floats') + + +def metric(value): + return value * MILLIMETERS_PER_INCH + +def inch(value): + return value / MILLIMETERS_PER_INCH -- cgit From e71d7a24b5be3e68d36494869595eec934db4bd2 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Wed, 18 Feb 2015 21:14:30 -0500 Subject: Python 3 tests passing --- gerber/utils.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'gerber/utils.py') diff --git a/gerber/utils.py b/gerber/utils.py index 542611d..8cd4965 100644 --- a/gerber/utils.py +++ b/gerber/utils.py @@ -74,7 +74,6 @@ def parse_gerber_value(value, format=(2, 5), zero_suppression='trailing'): raise ValueError('Parser only supports precision up to 6:7 format') # Remove extraneous information - #value = value.strip() value = value.lstrip('+') negative = '-' in value if negative: @@ -140,7 +139,7 @@ def write_gerber_value(value, format=(2, 5), zero_suppression='trailing'): digits = [val for val in fmtstring % value if val != '.'] # If all the digits are 0, return '0'. - digit_sum = reduce(lambda x,y:x+int(y), digits, 0) + digit_sum = sum([int(digit) for digit in digits]) if digit_sum == 0: return '0' -- cgit From 68619d4d5a7beb38dc81d953b43bf4196ca1d3a6 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Thu, 5 Mar 2015 22:42:42 -0500 Subject: Fix parsing for multiline ipc-d-356 records --- gerber/utils.py | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) (limited to 'gerber/utils.py') diff --git a/gerber/utils.py b/gerber/utils.py index 8cd4965..1c43550 100644 --- a/gerber/utils.py +++ b/gerber/utils.py @@ -26,6 +26,9 @@ files. # Author: Hamilton Kibbe # License: +from math import radians, sin, cos +from operator import sub + MILLIMETERS_PER_INCH = 25.4 def parse_gerber_value(value, format=(2, 5), zero_suppression='trailing'): @@ -238,7 +241,57 @@ def validate_coordinates(position): def metric(value): + """ Convert inch value to millimeters + + Parameters + ---------- + value : float + A value in inches. + + Returns + ------- + value : float + The equivalent value expressed in millimeters. + """ return value * MILLIMETERS_PER_INCH def inch(value): + """ Convert millimeter value to inches + + Parameters + ---------- + value : float + A value in millimeters. + + Returns + ------- + value : float + The equivalent value expressed in inches. + """ return value / MILLIMETERS_PER_INCH + + +def rotate_point(point, angle, center=(0.0, 0.0)): + """ Rotate a point about another point. + + Parameters + ----------- + point : tuple(, ) + Point to rotate about origin or center point + + angle : float + Angle to rotate the point [degrees] + + center : tuple(, ) + Coordinates about which the point is rotated. Defaults to the origin. + + Returns + ------- + rotated_point : tuple(, ) + `point` rotated about `center` by `angle` degrees. + """ + angle = radians(angle) + xdelta, ydelta = tuple(map(sub, point, center)) + x = center[0] + (cos(angle) * xdelta) - (sin(angle) * ydelta) + y = center[1] + (sin(angle) * xdelta) - (cos(angle) * ydelta) + return (x, y) -- cgit From b93804ed9a3400099afceacfe5a809ae8bded2a4 Mon Sep 17 00:00:00 2001 From: Paulo Henrique Silva Date: Tue, 7 Apr 2015 18:22:02 -0300 Subject: Add unspecified FS D leading zeros format FS D leading zero format (probably form Direct) is an unspecified coordinate format where all numbers are specified with both leading and trailing zeros. --- gerber/utils.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) (limited to 'gerber/utils.py') diff --git a/gerber/utils.py b/gerber/utils.py index 1c43550..df26516 100644 --- a/gerber/utils.py +++ b/gerber/utils.py @@ -54,7 +54,7 @@ def parse_gerber_value(value, format=(2, 5), zero_suppression='trailing'): (number of integer-part digits, number of decimal-part digits) zero_suppression : string - Zero-suppression mode. May be 'leading' or 'trailing' + Zero-suppression mode. May be 'leading', 'trailing' or 'none' Returns ------- @@ -82,11 +82,14 @@ def parse_gerber_value(value, format=(2, 5), zero_suppression='trailing'): if negative: value = value.lstrip('-') + missing_digits = MAX_DIGITS - len(value) - digits = list('0' * MAX_DIGITS) - offset = 0 if zero_suppression == 'trailing' else (MAX_DIGITS - len(value)) - for i, digit in enumerate(value): - digits[i + offset] = digit + if zero_suppression == 'trailing': + digits = list(value + ('0' * missing_digits)) + elif zero_suppression == 'leading': + digits = list(('0' * missing_digits) + value) + else: + digits = list(value) result = float(''.join(digits[:integer_digits] + ['.'] + digits[integer_digits:])) return -result if negative else result @@ -114,7 +117,7 @@ def write_gerber_value(value, format=(2, 5), zero_suppression='trailing'): (number of integer-part digits, number of decimal-part digits) zero_suppression : string - Zero-suppression mode. May be 'leading' or 'trailing' + Zero-suppression mode. May be 'leading', 'trailing' or 'none' Returns ------- @@ -150,7 +153,7 @@ def write_gerber_value(value, format=(2, 5), zero_suppression='trailing'): if zero_suppression == 'trailing': while digits and digits[-1] == '0': digits.pop() - else: + elif zero_suppression == 'leading': while digits and digits[0] == '0': digits.pop(0) -- cgit From dd63b169f177389602e17bc6ced53bd0f1ba0de3 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Sat, 10 Oct 2015 16:51:21 -0400 Subject: Allow files to be read from strings per #37 Adds a loads() method to the top level module which generates a GerberFile or ExcellonFile from a string --- gerber/utils.py | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) (limited to 'gerber/utils.py') diff --git a/gerber/utils.py b/gerber/utils.py index df26516..1c0af52 100644 --- a/gerber/utils.py +++ b/gerber/utils.py @@ -201,30 +201,20 @@ def decimal_string(value, precision=6, padding=False): return int(floatstr) -def detect_file_format(filename): +def detect_file_format(data): """ Determine format of a file Parameters ---------- - filename : string - Filename of the file to read. + data : string + string containing file data. Returns ------- format : string - File format. either 'excellon' or 'rs274x' + File format. 'excellon' or 'rs274x' or 'unknown' """ - - # Read the first 20 lines (if possible) - lines = [] - with open(filename, 'r') as f: - try: - for i in range(20): - lines.append(f.readline()) - except StopIteration: - pass - - # Look for + lines = data.split('\n') for line in lines: if 'M48' in line: return 'excellon' -- cgit From 6f876edd09d9b81649691e529f85653f14b8fd1c Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Tue, 22 Dec 2015 02:45:48 -0500 Subject: Add PCB interface this incorporates some of @chintal's layers.py changes PCB.from_directory() simplifies loading of multiple gerbers the PCB() class should be pretty helpful going forward... the context classes could use some cleaning up, although I'd like to wait until the freecad stuff gets merged, that way we can try to refactor the context base to support more use cases --- gerber/utils.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) (limited to 'gerber/utils.py') diff --git a/gerber/utils.py b/gerber/utils.py index 1c0af52..6653683 100644 --- a/gerber/utils.py +++ b/gerber/utils.py @@ -26,6 +26,7 @@ files. # Author: Hamilton Kibbe # License: +import os from math import radians, sin, cos from operator import sub @@ -219,7 +220,10 @@ def detect_file_format(data): if 'M48' in line: return 'excellon' elif '%FS' in line: - return'rs274x' + return 'rs274x' + elif ((len(line.split()) >= 2) and + (line.split()[0] == 'P') and (line.split()[1] == 'JOB')): + return 'ipc_d_356' return 'unknown' @@ -288,3 +292,13 @@ def rotate_point(point, angle, center=(0.0, 0.0)): x = center[0] + (cos(angle) * xdelta) - (sin(angle) * ydelta) y = center[1] + (sin(angle) * xdelta) - (cos(angle) * ydelta) return (x, y) + + +def listdir(directory, ignore_hidden=True, ignore_os=True): + os_files = ('.DS_Store', 'Thumbs.db', 'ethumbs.db') + files = os.listdir(directory) + if ignore_hidden: + files = [f for f in files if not f.startswith('.')] + if ignore_os: + files = [f for f in files if not f in os_files] + return files -- cgit From 29c0d82bf53907030d11df9eb09471b716a0be2e Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Sat, 27 Feb 2016 15:24:36 +0800 Subject: RS274X backend for rendering. Incompelte still --- gerber/utils.py | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'gerber/utils.py') diff --git a/gerber/utils.py b/gerber/utils.py index 1c0af52..16323d6 100644 --- a/gerber/utils.py +++ b/gerber/utils.py @@ -288,3 +288,9 @@ def rotate_point(point, angle, center=(0.0, 0.0)): x = center[0] + (cos(angle) * xdelta) - (sin(angle) * ydelta) y = center[1] + (sin(angle) * xdelta) - (cos(angle) * ydelta) return (x, y) + + +def nearly_equal(point1, point2, ndigits = 6): + '''Are the points nearly equal''' + + return round(point1[0] - point2[0], ndigits) == 0 and round(point1[1] - point2[1], ndigits) == 0 -- cgit From 223a010831f0d9dae4bd6d2e626a603a78eb0b1d Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Sat, 27 Feb 2016 18:18:04 +0800 Subject: Fix critical issue with rotatin points (when the angle is zero the y would be flipped). Render AM with outline to gerber --- gerber/utils.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) (limited to 'gerber/utils.py') diff --git a/gerber/utils.py b/gerber/utils.py index 16323d6..72bf2d1 100644 --- a/gerber/utils.py +++ b/gerber/utils.py @@ -284,10 +284,13 @@ def rotate_point(point, angle, center=(0.0, 0.0)): `point` rotated about `center` by `angle` degrees. """ angle = radians(angle) - xdelta, ydelta = tuple(map(sub, point, center)) - x = center[0] + (cos(angle) * xdelta) - (sin(angle) * ydelta) - y = center[1] + (sin(angle) * xdelta) - (cos(angle) * ydelta) - return (x, y) + + cos_angle = cos(angle) + sin_angle = sin(angle) + + return ( + cos_angle * (point[0] - center[0]) - sin_angle * (point[1] - center[1]) + center[0], + sin_angle * (point[0] - center[0]) + cos_angle * (point[1] - center[1]) + center[1]) def nearly_equal(point1, point2, ndigits = 6): -- cgit From af86c5c5a228855f40ecbf02074bbec65fd9b6d1 Mon Sep 17 00:00:00 2001 From: Garret Fick Date: Sat, 23 Apr 2016 13:32:32 +0800 Subject: Correctly find the center for single quadrant arcs --- gerber/utils.py | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'gerber/utils.py') diff --git a/gerber/utils.py b/gerber/utils.py index 72bf2d1..41d264a 100644 --- a/gerber/utils.py +++ b/gerber/utils.py @@ -297,3 +297,9 @@ def nearly_equal(point1, point2, ndigits = 6): '''Are the points nearly equal''' return round(point1[0] - point2[0], ndigits) == 0 and round(point1[1] - point2[1], ndigits) == 0 + +def sq_distance(point1, point2): + + diff1 = point1[0] - point2[0] + diff2 = point1[1] - point2[1] + return diff1 * diff1 + diff2 * diff2 -- cgit From 8cd842a41a55ab3d8f558a2e3e198beba7da58a1 Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Thu, 21 Jan 2016 03:57:44 -0500 Subject: Manually mere rendering changes --- gerber/utils.py | 41 +++++++++++++++++++++++++++++++++++------ 1 file changed, 35 insertions(+), 6 deletions(-) (limited to 'gerber/utils.py') diff --git a/gerber/utils.py b/gerber/utils.py index b968dc8..ef9c39e 100644 --- a/gerber/utils.py +++ b/gerber/utils.py @@ -23,15 +23,15 @@ This module provides utility functions for working with Gerber and Excellon files. """ -# Author: Hamilton Kibbe -# License: - import os from math import radians, sin, cos from operator import sub +from copy import deepcopy +from pyhull.convex_hull import ConvexHull MILLIMETERS_PER_INCH = 25.4 + def parse_gerber_value(value, format=(2, 5), zero_suppression='trailing'): """ Convert gerber/excellon formatted string to floating-point number @@ -92,7 +92,8 @@ def parse_gerber_value(value, format=(2, 5), zero_suppression='trailing'): else: digits = list(value) - result = float(''.join(digits[:integer_digits] + ['.'] + digits[integer_digits:])) + result = float( + ''.join(digits[:integer_digits] + ['.'] + digits[integer_digits:])) return -result if negative else result @@ -132,7 +133,8 @@ def write_gerber_value(value, format=(2, 5), zero_suppression='trailing'): if MAX_DIGITS > 13 or integer_digits > 6 or decimal_digits > 7: raise ValueError('Parser only supports precision up to 6:7 format') - # Edge case... (per Gerber spec we should return 0 in all cases, see page 77) + # Edge case... (per Gerber spec we should return 0 in all cases, see page + # 77) if value == 0: return '0' @@ -222,7 +224,7 @@ def detect_file_format(data): elif '%FS' in line: return 'rs274x' elif ((len(line.split()) >= 2) and - (line.split()[0] == 'P') and (line.split()[1] == 'JOB')): + (line.split()[0] == 'P') and (line.split()[1] == 'JOB')): return 'ipc_d_356' return 'unknown' @@ -252,6 +254,7 @@ def metric(value): """ return value * MILLIMETERS_PER_INCH + def inch(value): """ Convert millimeter value to inches @@ -310,6 +313,26 @@ def sq_distance(point1, point2): def listdir(directory, ignore_hidden=True, ignore_os=True): + """ List files in given directory. + Differs from os.listdir() in that hidden and OS-generated files are ignored + by default. + + Parameters + ---------- + directory : str + path to the directory for which to list files. + + ignore_hidden : bool + If True, ignore files beginning with a leading '.' + + ignore_os : bool + If True, ignore OS-generated files, e.g. Thumbs.db + + Returns + ------- + files : list + list of files in specified directory + """ os_files = ('.DS_Store', 'Thumbs.db', 'ethumbs.db') files = os.listdir(directory) if ignore_hidden: @@ -317,3 +340,9 @@ def listdir(directory, ignore_hidden=True, ignore_os=True): if ignore_os: files = [f for f in files if not f in os_files] return files + + +def convex_hull(points): + vertices = ConvexHull(points).vertices + return [points[idx] for idx in + set([point for pair in vertices for point in pair])] -- cgit