252 lines
8.1 KiB
Python
252 lines
8.1 KiB
Python
import math
|
|
|
|
from .svgpath import parse_path
|
|
|
|
|
|
class ExtraFieldData(object):
|
|
def __init__(self, fields, fields_by_ref, fields_by_index=None):
|
|
self.fields = fields
|
|
self.fields_by_ref = fields_by_ref
|
|
self.fields_by_index = fields_by_index
|
|
|
|
|
|
class EcadParser(object):
|
|
|
|
def __init__(self, file_name, config, logger):
|
|
"""
|
|
:param file_name: path to file that should be parsed.
|
|
:param config: Config instance
|
|
:param logger: logging object.
|
|
"""
|
|
self.file_name = file_name
|
|
self.config = config
|
|
self.logger = logger
|
|
|
|
def parse(self):
|
|
"""
|
|
Abstract method that should be overridden in implementations.
|
|
Performs all the parsing and returns a tuple of
|
|
(pcbdata, components)
|
|
pcbdata is described in DATAFORMAT.md
|
|
components is list of Component objects
|
|
:return:
|
|
"""
|
|
pass
|
|
|
|
@staticmethod
|
|
def normalize_field_names(data):
|
|
# type: (ExtraFieldData) -> ExtraFieldData
|
|
def remap(ref_fields):
|
|
return {f.lower(): v for (f, v) in
|
|
sorted(ref_fields.items(), reverse=True) if v}
|
|
|
|
by_ref = {r: remap(d) for (r, d) in data.fields_by_ref.items()}
|
|
if data.fields_by_index:
|
|
by_index = {i: remap(d) for (i, d) in data.fields_by_index.items()}
|
|
print([a.get("blah", "") for a in by_index.values()])
|
|
else:
|
|
by_index = None
|
|
|
|
field_map = {f.lower(): f for f in sorted(data.fields, reverse=True)}
|
|
return ExtraFieldData(field_map.values(), by_ref, by_index)
|
|
|
|
def get_extra_field_data(self, file_name):
|
|
"""
|
|
Abstract method that may be overridden in implementations that support
|
|
extra field data.
|
|
:return: ExtraFieldData
|
|
"""
|
|
return ExtraFieldData([], {})
|
|
|
|
def parse_extra_data(self, file_name, normalize_case):
|
|
"""
|
|
Parses the file and returns extra field data.
|
|
:param file_name: path to file containing extra data
|
|
:param normalize_case: if true, normalize case so that
|
|
"mpn", "Mpn", "MPN" fields are combined
|
|
:return:
|
|
"""
|
|
data = self.get_extra_field_data(file_name)
|
|
if normalize_case:
|
|
data = self.normalize_field_names(data)
|
|
return ExtraFieldData(
|
|
sorted(data.fields), data.fields_by_ref, data.fields_by_index)
|
|
|
|
def latest_extra_data(self, extra_dirs=None):
|
|
"""
|
|
Abstract method that may be overridden in implementations that support
|
|
extra field data.
|
|
:param extra_dirs: List of extra directories to search.
|
|
:return: File name of most recent file with extra field data.
|
|
"""
|
|
return None
|
|
|
|
def extra_data_file_filter(self):
|
|
"""
|
|
Abstract method that may be overridden in implementations that support
|
|
extra field data.
|
|
:return: File open dialog filter string, eg:
|
|
"Netlist and xml files (*.net; *.xml)|*.net;*.xml"
|
|
"""
|
|
return None
|
|
|
|
def add_drawing_bounding_box(self, drawing, bbox):
|
|
# type: (dict, BoundingBox) -> None
|
|
|
|
def add_segment():
|
|
bbox.add_segment(drawing['start'][0], drawing['start'][1],
|
|
drawing['end'][0], drawing['end'][1],
|
|
drawing['width'] / 2)
|
|
|
|
def add_circle():
|
|
bbox.add_circle(drawing['start'][0], drawing['start'][1],
|
|
drawing['radius'] + drawing['width'] / 2)
|
|
|
|
def add_svgpath():
|
|
width = drawing.get('width', 0)
|
|
bbox.add_svgpath(drawing['svgpath'], width, self.logger)
|
|
|
|
def add_polygon():
|
|
if 'polygons' not in drawing:
|
|
add_svgpath()
|
|
return
|
|
polygon = drawing['polygons'][0]
|
|
for point in polygon:
|
|
bbox.add_point(point[0], point[1])
|
|
|
|
def add_arc():
|
|
if 'svgpath' in drawing:
|
|
add_svgpath()
|
|
else:
|
|
width = drawing.get('width', 0)
|
|
xc, yc = drawing['start'][:2]
|
|
a1 = drawing['startangle']
|
|
a2 = drawing['endangle']
|
|
r = drawing['radius']
|
|
x1 = xc + math.cos(math.radians(a1))
|
|
y1 = xc + math.sin(math.radians(a1))
|
|
x2 = xc + math.cos(math.radians(a2))
|
|
y2 = xc + math.sin(math.radians(a2))
|
|
da = a2 - a1 if a2 > a1 else a2 + 360 - a1
|
|
la = 1 if da > 180 else 0
|
|
svgpath = 'M %s %s A %s %s 0 %s 1 %s %s' % \
|
|
(x1, y1, r, r, la, x2, y2)
|
|
bbox.add_svgpath(svgpath, width, self.logger)
|
|
|
|
{
|
|
'segment': add_segment,
|
|
'rect': add_segment, # bbox of a rect and segment are the same
|
|
'circle': add_circle,
|
|
'arc': add_arc,
|
|
'polygon': add_polygon,
|
|
'text': lambda: None, # text is not really needed for bounding box
|
|
}.get(drawing['type'])()
|
|
|
|
|
|
class Component(object):
|
|
"""Simple data object to store component data needed for bom table."""
|
|
|
|
def __init__(self, ref, val, footprint, layer, attr=None, extra_fields={}):
|
|
self.ref = ref
|
|
self.val = val
|
|
self.footprint = footprint
|
|
self.layer = layer
|
|
self.attr = attr
|
|
self.extra_fields = extra_fields
|
|
|
|
|
|
class BoundingBox(object):
|
|
"""Geometry util to calculate and combine bounding box of simple shapes."""
|
|
|
|
def __init__(self):
|
|
self._x0 = None
|
|
self._y0 = None
|
|
self._x1 = None
|
|
self._y1 = None
|
|
|
|
def to_dict(self):
|
|
# type: () -> dict
|
|
return {
|
|
"minx": self._x0,
|
|
"miny": self._y0,
|
|
"maxx": self._x1,
|
|
"maxy": self._y1,
|
|
}
|
|
|
|
def to_component_dict(self):
|
|
# type: () -> dict
|
|
return {
|
|
"pos": [self._x0, self._y0],
|
|
"relpos": [0, 0],
|
|
"size": [self._x1 - self._x0, self._y1 - self._y0],
|
|
"angle": 0,
|
|
}
|
|
|
|
def add(self, other):
|
|
"""Add another bounding box.
|
|
:type other: BoundingBox
|
|
"""
|
|
if other._x0 is not None:
|
|
self.add_point(other._x0, other._y0)
|
|
self.add_point(other._x1, other._y1)
|
|
return self
|
|
|
|
@staticmethod
|
|
def _rotate(x, y, rx, ry, angle):
|
|
sin = math.sin(math.radians(angle))
|
|
cos = math.cos(math.radians(angle))
|
|
new_x = rx + (x - rx) * cos - (y - ry) * sin
|
|
new_y = ry + (x - rx) * sin + (y - ry) * cos
|
|
return new_x, new_y
|
|
|
|
def add_point(self, x, y, rx=0, ry=0, angle=0):
|
|
x, y = self._rotate(x, y, rx, ry, angle)
|
|
if self._x0 is None:
|
|
self._x0 = x
|
|
self._y0 = y
|
|
self._x1 = x
|
|
self._y1 = y
|
|
else:
|
|
self._x0 = min(self._x0, x)
|
|
self._y0 = min(self._y0, y)
|
|
self._x1 = max(self._x1, x)
|
|
self._y1 = max(self._y1, y)
|
|
return self
|
|
|
|
def add_segment(self, x0, y0, x1, y1, r):
|
|
self.add_circle(x0, y0, r)
|
|
self.add_circle(x1, y1, r)
|
|
return self
|
|
|
|
def add_rectangle(self, x, y, w, h, angle=0):
|
|
self.add_point(x - w / 2, y - h / 2, x, y, angle)
|
|
self.add_point(x + w / 2, y - h / 2, x, y, angle)
|
|
self.add_point(x - w / 2, y + h / 2, x, y, angle)
|
|
self.add_point(x + w / 2, y + h / 2, x, y, angle)
|
|
return self
|
|
|
|
def add_circle(self, x, y, r):
|
|
self.add_point(x - r, y)
|
|
self.add_point(x, y - r)
|
|
self.add_point(x + r, y)
|
|
self.add_point(x, y + r)
|
|
return self
|
|
|
|
def add_svgpath(self, svgpath, width, logger):
|
|
w = width / 2
|
|
for segment in parse_path(svgpath, logger):
|
|
x0, x1, y0, y1 = segment.bbox()
|
|
self.add_point(x0 - w, y0 - w)
|
|
self.add_point(x1 + w, y1 + w)
|
|
|
|
def pad(self, amount):
|
|
"""Add small padding to the box."""
|
|
if self._x0 is not None:
|
|
self._x0 -= amount
|
|
self._y0 -= amount
|
|
self._x1 += amount
|
|
self._y1 += amount
|
|
|
|
def initialized(self):
|
|
return self._x0 is not None
|