0321 from macmini
This commit is contained in:
920
3rdparty/plugins/org_openscopeproject_InteractiveHtmlBom/ecad/fusion_eagle.py
vendored
Normal file
920
3rdparty/plugins/org_openscopeproject_InteractiveHtmlBom/ecad/fusion_eagle.py
vendored
Normal file
@ -0,0 +1,920 @@
|
||||
import io
|
||||
import math
|
||||
import os
|
||||
import string
|
||||
import zipfile
|
||||
from datetime import datetime
|
||||
from xml.etree import ElementTree
|
||||
|
||||
from .common import EcadParser, Component, BoundingBox
|
||||
from .svgpath import Arc
|
||||
from ..core.fontparser import FontParser
|
||||
|
||||
|
||||
class FusionEagleParser(EcadParser):
|
||||
TOP_COPPER_LAYER = '1'
|
||||
BOT_COPPER_LAYER = '16'
|
||||
TOP_PLACE_LAYER = '21'
|
||||
BOT_PLACE_LAYER = '22'
|
||||
TOP_NAMES_LAYER = '25'
|
||||
BOT_NAMES_LAYER = '26'
|
||||
DIMENSION_LAYER = '20'
|
||||
TOP_DOCU_LAYER = '51'
|
||||
BOT_DOCU_LAYER = '52'
|
||||
|
||||
def __init__(self, file_name, config, logger):
|
||||
super(FusionEagleParser, self).__init__(file_name, config, logger)
|
||||
self.config = config
|
||||
self.font_parser = FontParser()
|
||||
self.min_via_w = 1e-3
|
||||
self.pcbdata = {
|
||||
'drawings': {
|
||||
'silkscreen': {
|
||||
'F': [],
|
||||
'B': []
|
||||
},
|
||||
'fabrication': {
|
||||
'F': [],
|
||||
'B': []
|
||||
}
|
||||
},
|
||||
'edges': [],
|
||||
'footprints': [],
|
||||
'font_data': {}
|
||||
}
|
||||
self.components = []
|
||||
|
||||
def _parse_pad_nets(self, signals):
|
||||
elements = {}
|
||||
|
||||
for signal in signals.iter('signal'):
|
||||
net = signal.attrib['name']
|
||||
for c in signal.iter('contactref'):
|
||||
e = c.attrib['element']
|
||||
if e not in elements:
|
||||
elements[e] = {}
|
||||
elements[e][c.attrib['pad']] = net
|
||||
|
||||
self.elements_pad_nets = elements
|
||||
|
||||
@staticmethod
|
||||
def _radian(ux, uy, vx, vy):
|
||||
dot = ux * vx + uy * vy
|
||||
mod = math.sqrt((ux * ux + uy * uy) * (vx * vx + vy * vy))
|
||||
rad = math.acos(dot / mod)
|
||||
if ux * vy - uy * vx < 0.0:
|
||||
rad = -rad
|
||||
return rad
|
||||
|
||||
def _curve_to_svgparams(self, el, x=0, y=0, angle=0, mirrored=False):
|
||||
_x1 = float(el.attrib['x1'])
|
||||
_x2 = float(el.attrib['x2'])
|
||||
_y1 = -float(el.attrib['y1'])
|
||||
_y2 = -float(el.attrib['y2'])
|
||||
|
||||
dx1, dy1 = self._rotate(_x1, _y1, -angle, mirrored)
|
||||
dx2, dy2 = self._rotate(_x2, _y2, -angle, mirrored)
|
||||
|
||||
x1, y1 = x + dx1, -y + dy1
|
||||
x2, y2 = x + dx2, -y + dy2
|
||||
|
||||
chord = math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)
|
||||
theta = float(el.attrib['curve'])
|
||||
theta = -theta if mirrored else theta
|
||||
r = abs(0.5 * chord / math.sin(math.radians(theta) / 2))
|
||||
la = 0 if abs(theta) < 180 else 1
|
||||
sw = 0 if theta > 0 else 1
|
||||
return {
|
||||
'x1': x1,
|
||||
'y1': y1,
|
||||
'r': r,
|
||||
'la': la,
|
||||
'sw': sw,
|
||||
'x2': x2,
|
||||
'y2': y2
|
||||
}
|
||||
|
||||
def _curve_to_svgpath(self, el, x=0, y=0, angle=0, mirrored=False):
|
||||
p = self._curve_to_svgparams(el, x, y, angle, mirrored)
|
||||
return 'M {x1} {y1} A {r} {r} 0 {la} {sw} {x2} {y2}'.format(**p)
|
||||
|
||||
@staticmethod
|
||||
class Rot:
|
||||
def __init__(self, rot_string):
|
||||
if not rot_string:
|
||||
self.mirrored = False
|
||||
self.spin = False
|
||||
self.angle = 0
|
||||
return
|
||||
self.mirrored = 'M' in rot_string
|
||||
self.spin = 'S' in rot_string
|
||||
self.angle = float(''.join(d for d in rot_string
|
||||
if d in string.digits + '.'))
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
def __str__(self):
|
||||
return "Mirrored: {0}, Spin: {1}, Angle: {2}".format(self.mirrored,
|
||||
self.spin,
|
||||
self.angle)
|
||||
|
||||
def _rectangle_vertices(self, el):
|
||||
# Note: Eagle specifies a rectangle using opposing corners
|
||||
# (x1, y1) = lower-left and (x2, y2) = upper-right) and *optionally*
|
||||
# a rotation angle. The size of the rectangle is defined by the
|
||||
# corners irrespective of rotation angle, and then it is rotated
|
||||
# about its own center point.
|
||||
_x1 = float(el.attrib['x1'])
|
||||
_x2 = float(el.attrib['x2'])
|
||||
_y1 = -float(el.attrib['y1'])
|
||||
_y2 = -float(el.attrib['y2'])
|
||||
|
||||
# Center of rectangle
|
||||
xc = (_x1 + _x2) / 2
|
||||
yc = (_y1 + _y2) / 2
|
||||
|
||||
# Vertices of rectangle relative to its center, un-rotated
|
||||
_dv_c = [
|
||||
(_x1 - xc, _y1 - yc),
|
||||
(_x2 - xc, _y1 - yc),
|
||||
(_x2 - xc, _y2 - yc),
|
||||
(_x1 - xc, _y2 - yc)
|
||||
]
|
||||
|
||||
elr = self.Rot(el.get('rot'))
|
||||
|
||||
# Rotate the rectangle about its center
|
||||
dv_c = [self._rotate(_x, _y, -elr.angle, elr.mirrored)
|
||||
for (_x, _y) in _dv_c]
|
||||
|
||||
# Map vertices to position relative to component origin, un-rotated
|
||||
return [(_x + xc, _y + yc) for (_x, _y) in dv_c]
|
||||
|
||||
def _add_drawing(self, el):
|
||||
layer_dest = {
|
||||
self.DIMENSION_LAYER: self.pcbdata['edges'],
|
||||
self.TOP_PLACE_LAYER: self.pcbdata['drawings']['silkscreen']['F'],
|
||||
self.BOT_PLACE_LAYER: self.pcbdata['drawings']['silkscreen']['B'],
|
||||
self.TOP_NAMES_LAYER: self.pcbdata['drawings']['silkscreen']['F'],
|
||||
self.BOT_NAMES_LAYER: self.pcbdata['drawings']['silkscreen']['B']
|
||||
}
|
||||
if ("layer" in el.attrib) and (el.attrib['layer'] in layer_dest):
|
||||
dwg = None
|
||||
if el.tag == 'wire':
|
||||
dwg = {'width': float(el.attrib['width'])}
|
||||
|
||||
if 'curve' in el.attrib:
|
||||
dwg['type'] = 'arc'
|
||||
dwg['svgpath'] = self._curve_to_svgpath(el)
|
||||
else:
|
||||
dwg['type'] = 'segment'
|
||||
dwg['start'] = [
|
||||
float(el.attrib['x1']),
|
||||
-float(el.attrib['y1'])
|
||||
]
|
||||
dwg['end'] = [
|
||||
float(el.attrib['x2']),
|
||||
-float(el.attrib['y2'])
|
||||
]
|
||||
|
||||
elif el.tag == 'text':
|
||||
# Text is not currently supported (except refdes)
|
||||
# due to lack of Eagle font data
|
||||
pass
|
||||
|
||||
elif el.tag == 'circle':
|
||||
dwg = {
|
||||
'type': 'circle',
|
||||
'start': [float(el.attrib['x']), -float(el.attrib['y'])],
|
||||
'radius': float(el.attrib['radius']),
|
||||
'width': float(el.attrib['width'])
|
||||
}
|
||||
|
||||
elif el.tag in ['polygonshape', 'polygon']:
|
||||
dwg = {
|
||||
'type': 'polygon',
|
||||
'pos': [0, 0],
|
||||
'angle': 0,
|
||||
'polygons': []
|
||||
}
|
||||
segs = el if el.tag == 'polygon' \
|
||||
else el.find('polygonoutlinesegments')
|
||||
polygon = FusionEagleParser._segments_to_polygon(segs)
|
||||
dwg['polygons'].append(polygon)
|
||||
|
||||
elif el.tag == 'rectangle':
|
||||
vertices = self._rectangle_vertices(el)
|
||||
dwg = {
|
||||
'type': 'polygon',
|
||||
'pos': [0, 0],
|
||||
'angle': 0,
|
||||
'polygons': [[list(v) for v in vertices]]
|
||||
}
|
||||
|
||||
if dwg:
|
||||
layer_dest[el.attrib['layer']].append(dwg)
|
||||
|
||||
def _add_track(self, el, net):
|
||||
if el.tag == 'via' or (
|
||||
el.tag == 'wire' and el.attrib['layer'] in
|
||||
[self.TOP_COPPER_LAYER, self.BOT_COPPER_LAYER]):
|
||||
trk = {}
|
||||
if self.config.include_nets:
|
||||
trk['net'] = net
|
||||
|
||||
if el.tag == 'wire':
|
||||
dest = self.pcbdata['tracks']['F'] \
|
||||
if el.attrib['layer'] == self.TOP_COPPER_LAYER\
|
||||
else self.pcbdata['tracks']['B']
|
||||
|
||||
if 'curve' in el.attrib:
|
||||
trk['width'] = float(el.attrib['width'])
|
||||
# Get SVG parameters for the curve
|
||||
p = self._curve_to_svgparams(el)
|
||||
start = complex(p['x1'], p['y1'])
|
||||
end = complex(p['x2'], p['y2'])
|
||||
radius = complex(p['r'], p['r'])
|
||||
large_arc = bool(p['la'])
|
||||
sweep = bool(p['sw'])
|
||||
# Pass SVG parameters to get center parameters
|
||||
arc = Arc(start, radius, 0, large_arc, sweep, end)
|
||||
# Create arc track from center parameters
|
||||
trk['center'] = [arc.center.real, arc.center.imag]
|
||||
trk['radius'] = radius.real
|
||||
if arc.delta < 0:
|
||||
trk['startangle'] = arc.theta + arc.delta
|
||||
trk['endangle'] = arc.theta
|
||||
else:
|
||||
trk['startangle'] = arc.theta
|
||||
trk['endangle'] = arc.theta + arc.delta
|
||||
dest.append(trk)
|
||||
else:
|
||||
trk['start'] = [
|
||||
float(el.attrib['x1']),
|
||||
-float(el.attrib['y1'])
|
||||
]
|
||||
trk['end'] = [
|
||||
float(el.attrib['x2']),
|
||||
-float(el.attrib['y2'])
|
||||
]
|
||||
trk['width'] = float(el.attrib['width'])
|
||||
dest.append(trk)
|
||||
|
||||
elif el.tag == 'via':
|
||||
trk['start'] = [float(el.attrib['x']), -float(el.attrib['y'])]
|
||||
trk['end'] = trk['start']
|
||||
trk['width'] = float(el.attrib['drill']) + 2 * self.min_via_w \
|
||||
if 'diameter' not in el.attrib else float(
|
||||
el.attrib['diameter'])
|
||||
if float(el.attrib['drill']) >= self.min_drill_via_untented:
|
||||
trk['drillsize'] = float(el.attrib['drill'])
|
||||
self.pcbdata['tracks']['F'].append(trk)
|
||||
self.pcbdata['tracks']['B'].append(trk)
|
||||
|
||||
def _calculate_footprint_bbox(self, package, x, y, angle, mirrored):
|
||||
_angle = angle if not mirrored else -angle
|
||||
layers = [
|
||||
self.TOP_PLACE_LAYER,
|
||||
self.BOT_PLACE_LAYER,
|
||||
self.TOP_DOCU_LAYER,
|
||||
self.BOT_DOCU_LAYER
|
||||
]
|
||||
xmax, ymax = -float('inf'), -float('inf')
|
||||
xmin, ymin = float('inf'), float('inf')
|
||||
|
||||
for el in package.iter('wire'):
|
||||
if el.tag == 'wire' and el.attrib['layer'] in layers:
|
||||
xmax = max(xmax,
|
||||
max(float(el.attrib['x1']), float(el.attrib['x2'])))
|
||||
ymax = max(ymax,
|
||||
max(float(el.attrib['y1']), float(el.attrib['y2'])))
|
||||
xmin = min(xmin,
|
||||
min(float(el.attrib['x1']), float(el.attrib['x2'])))
|
||||
ymin = min(ymin,
|
||||
min(float(el.attrib['y1']), float(el.attrib['y2'])))
|
||||
|
||||
for el in package.iter():
|
||||
if el.tag in ['smd', 'pad']:
|
||||
elx, ely = float(el.attrib['x']), float(el.attrib['y'])
|
||||
if el.tag == 'smd':
|
||||
dx, dy = abs(float(el.attrib['dx'])) / 2, abs(
|
||||
float(el.attrib['dy'])) / 2
|
||||
else:
|
||||
d = el.get('diameter')
|
||||
if d is None:
|
||||
diameter = float(el.get('drill')) + 2 * self.min_via_w
|
||||
else:
|
||||
diameter = float(d)
|
||||
dx, dy = diameter / 2, diameter / 2
|
||||
xmax, ymax = max(xmax, elx + dx), max(ymax, ely + dy)
|
||||
xmin, ymin = min(xmin, elx - dx), min(ymin, ely - dy)
|
||||
|
||||
if not math.isinf(xmin):
|
||||
if mirrored:
|
||||
xmin, xmax = -xmax, -xmin
|
||||
dx, dy = self._rotate(xmin, ymax, _angle)
|
||||
sx = abs(xmax - xmin)
|
||||
sy = abs(ymax - ymin)
|
||||
else:
|
||||
dx, dy = 0, 0
|
||||
sx, sy = 0, 0
|
||||
|
||||
return {
|
||||
'pos': [x + dx, -y - dy],
|
||||
'angle': _angle,
|
||||
'relpos': [0, 0],
|
||||
'size': [sx, sy]
|
||||
}
|
||||
|
||||
def _footprint_pads(self, package, x, y, angle, mirrored, refdes):
|
||||
pads = []
|
||||
element_pad_nets = self.elements_pad_nets.get(refdes)
|
||||
pin1_allocated = False
|
||||
for el in package.iter():
|
||||
if el.tag == 'pad':
|
||||
elx = float(el.attrib['x'])
|
||||
ely = -float(el.attrib['y'])
|
||||
drill = float(el.attrib['drill'])
|
||||
|
||||
dx, dy = self._rotate(elx, ely, -angle, mirrored)
|
||||
|
||||
diameter = drill + 2 * self.min_via_w \
|
||||
if 'diameter' not in el.attrib \
|
||||
else float(el.attrib['diameter'])
|
||||
|
||||
pr = self.Rot(el.get('rot'))
|
||||
if mirrored ^ pr.mirrored:
|
||||
pad_angle = -angle - pr.angle
|
||||
else:
|
||||
pad_angle = angle + pr.angle
|
||||
|
||||
pad = {
|
||||
'layers': ['F', 'B'],
|
||||
'pos': [x + dx, -y + dy],
|
||||
'angle': pad_angle,
|
||||
'type': 'th',
|
||||
'drillshape': 'circle',
|
||||
'drillsize': [
|
||||
drill,
|
||||
drill
|
||||
]
|
||||
}
|
||||
|
||||
if el.get('name') in ['1', 'A', 'A1', 'P1', 'PAD1'] and \
|
||||
not pin1_allocated:
|
||||
pad['pin1'] = 1
|
||||
pin1_allocated = True
|
||||
|
||||
if 'shape' not in el.attrib or el.attrib['shape'] == 'round':
|
||||
pad['shape'] = 'circle'
|
||||
pad['size'] = [diameter, diameter]
|
||||
elif el.attrib['shape'] == 'square':
|
||||
pad['shape'] = 'rect'
|
||||
pad['size'] = [diameter, diameter]
|
||||
elif el.attrib['shape'] == 'octagon':
|
||||
pad['shape'] = 'chamfrect'
|
||||
pad['size'] = [diameter, diameter]
|
||||
pad['radius'] = 0
|
||||
pad['chamfpos'] = 0b1111 # all corners
|
||||
pad['chamfratio'] = 0.333
|
||||
elif el.attrib['shape'] == 'long':
|
||||
pad['shape'] = 'roundrect'
|
||||
pad['radius'] = diameter / 2
|
||||
pad['size'] = [2 * diameter, diameter]
|
||||
elif el.attrib['shape'] == 'offset':
|
||||
pad['shape'] = 'roundrect'
|
||||
pad['radius'] = diameter / 2
|
||||
pad['size'] = [2 * diameter, diameter]
|
||||
pad['offset'] = [diameter / 2, 0]
|
||||
elif el.attrib['shape'] == 'slot':
|
||||
pad['shape'] = 'roundrect'
|
||||
pad['radius'] = diameter / 2
|
||||
slot_length = float(el.attrib['slotLength'])
|
||||
pad['size'] = [slot_length + diameter / 2, diameter]
|
||||
pad['drillshape'] = 'oblong'
|
||||
pad['drillsize'] = [slot_length, drill]
|
||||
else:
|
||||
self.logger.info(
|
||||
"Unsupported footprint pad shape %s, skipping",
|
||||
el.attrib['shape'])
|
||||
|
||||
if self.config.include_nets and element_pad_nets is not None:
|
||||
net = element_pad_nets.get(el.attrib['name'])
|
||||
if net is not None:
|
||||
pad['net'] = net
|
||||
|
||||
pads.append(pad)
|
||||
|
||||
elif el.tag == 'smd':
|
||||
layer = el.attrib['layer']
|
||||
if layer == self.TOP_COPPER_LAYER and not mirrored or \
|
||||
layer == self.BOT_COPPER_LAYER and mirrored:
|
||||
layers = ['F']
|
||||
elif layer == self.TOP_COPPER_LAYER and mirrored or \
|
||||
layer == self.BOT_COPPER_LAYER and not mirrored:
|
||||
layers = ['B']
|
||||
else:
|
||||
self.logger.error('Unable to determine layer for '
|
||||
'{0} pad {1}'.format(refdes,
|
||||
el.attrib['name']))
|
||||
layers = None
|
||||
|
||||
if layers is not None:
|
||||
elx = float(el.attrib['x'])
|
||||
ely = -float(el.attrib['y'])
|
||||
|
||||
dx, dy = self._rotate(elx, ely, -angle, mirrored)
|
||||
|
||||
pr = self.Rot(el.get('rot'))
|
||||
if mirrored ^ pr.mirrored:
|
||||
pad_angle = -angle - pr.angle
|
||||
else:
|
||||
pad_angle = angle + pr.angle
|
||||
|
||||
pad = {'layers': layers,
|
||||
'pos': [x + dx, -y + dy],
|
||||
'size': [
|
||||
float(el.attrib['dx']),
|
||||
float(el.attrib['dy'])
|
||||
],
|
||||
'angle': pad_angle,
|
||||
'type': 'smd',
|
||||
}
|
||||
|
||||
if el.get('name') in ['1', 'A', 'A1', 'P1', 'PAD1'] and \
|
||||
not pin1_allocated:
|
||||
pad['pin1'] = 1
|
||||
pin1_allocated = True
|
||||
|
||||
if 'roundness' not in el.attrib:
|
||||
pad['shape'] = 'rect'
|
||||
else:
|
||||
pad['shape'] = 'roundrect'
|
||||
pad['radius'] = (float(el.attrib['roundness']) / 100) \
|
||||
* float(el.attrib['dy']) / 2
|
||||
|
||||
if self.config.include_nets and \
|
||||
element_pad_nets is not None:
|
||||
net = element_pad_nets.get(el.attrib['name'])
|
||||
if net is not None:
|
||||
pad['net'] = net
|
||||
|
||||
pads.append(pad)
|
||||
return pads
|
||||
|
||||
@staticmethod
|
||||
def _rotate(x, y, angle, mirrored=False):
|
||||
sin = math.sin(math.radians(angle))
|
||||
cos = math.cos(math.radians(angle))
|
||||
xr = x * cos - y * sin
|
||||
yr = y * cos + x * sin
|
||||
if mirrored:
|
||||
return -xr, yr
|
||||
else:
|
||||
return xr, yr
|
||||
|
||||
def _process_footprint(self, package, x, y, angle, mirrored, populate):
|
||||
for el in package.iter():
|
||||
if el.tag in ['wire', 'rectangle', 'circle', 'hole',
|
||||
'polygonshape', 'polygon', 'hole']:
|
||||
if el.tag == 'hole':
|
||||
dwg_layer = self.pcbdata['edges']
|
||||
elif el.attrib['layer'] in [self.TOP_PLACE_LAYER,
|
||||
self.BOT_PLACE_LAYER]:
|
||||
dwg_layer = self.pcbdata['drawings']['silkscreen']
|
||||
top = el.attrib['layer'] == self.TOP_PLACE_LAYER
|
||||
elif el.attrib['layer'] in [self.TOP_DOCU_LAYER,
|
||||
self.BOT_DOCU_LAYER]:
|
||||
if not populate:
|
||||
return
|
||||
dwg_layer = self.pcbdata['drawings']['fabrication']
|
||||
top = el.attrib['layer'] == self.TOP_DOCU_LAYER
|
||||
elif el.tag == 'wire' and \
|
||||
el.attrib['layer'] == self.DIMENSION_LAYER:
|
||||
dwg_layer = self.pcbdata['edges']
|
||||
top = True
|
||||
else:
|
||||
continue
|
||||
|
||||
dwg = None
|
||||
|
||||
if el.tag == 'wire':
|
||||
_dx1 = float(el.attrib['x1'])
|
||||
_dx2 = float(el.attrib['x2'])
|
||||
_dy1 = -float(el.attrib['y1'])
|
||||
_dy2 = -float(el.attrib['y2'])
|
||||
|
||||
dx1, dy1 = self._rotate(_dx1, _dy1, -angle, mirrored)
|
||||
dx2, dy2 = self._rotate(_dx2, _dy2, -angle, mirrored)
|
||||
|
||||
x1, y1 = x + dx1, -y + dy1
|
||||
x2, y2 = x + dx2, -y + dy2
|
||||
|
||||
if el.get('curve'):
|
||||
dwg = {
|
||||
'type': 'arc',
|
||||
'width': float(el.attrib['width']),
|
||||
'svgpath': self._curve_to_svgpath(el, x, y, angle,
|
||||
mirrored)
|
||||
}
|
||||
else:
|
||||
dwg = {
|
||||
'type': 'segment',
|
||||
'start': [x1, y1],
|
||||
'end': [x2, y2],
|
||||
'width': float(el.attrib['width'])
|
||||
}
|
||||
|
||||
elif el.tag == 'rectangle':
|
||||
_dv = self._rectangle_vertices(el)
|
||||
|
||||
# Rotate rectangle about component origin based on
|
||||
# component angle
|
||||
dv = [self._rotate(_x, _y, -angle, mirrored)
|
||||
for (_x, _y) in _dv]
|
||||
|
||||
# Map vertices back to absolute coordinates
|
||||
v = [(x + _x, -y + _y) for (_x, _y) in dv]
|
||||
|
||||
dwg = {
|
||||
'type': 'polygon',
|
||||
'filled': 1,
|
||||
'pos': [0, 0],
|
||||
'polygons': [v]
|
||||
}
|
||||
|
||||
elif el.tag in ['circle', 'hole']:
|
||||
_x = float(el.attrib['x'])
|
||||
_y = -float(el.attrib['y'])
|
||||
dxc, dyc = self._rotate(_x, _y, -angle, mirrored)
|
||||
xc, yc = x + dxc, -y + dyc
|
||||
|
||||
if el.tag == 'circle':
|
||||
radius = float(el.attrib['radius'])
|
||||
width = float(el.attrib['width'])
|
||||
else:
|
||||
radius = float(el.attrib['drill']) / 2
|
||||
width = 0
|
||||
|
||||
dwg = {
|
||||
'type': 'circle',
|
||||
'start': [xc, yc],
|
||||
'radius': radius,
|
||||
'width': width
|
||||
}
|
||||
|
||||
elif el.tag in ['polygonshape', 'polygon']:
|
||||
segs = el if el.tag == 'polygon' \
|
||||
else el.find('polygonoutlinesegments')
|
||||
|
||||
dv = self._segments_to_polygon(segs, angle, mirrored)
|
||||
|
||||
polygon = [[x + v[0], -y + v[1]] for v in dv]
|
||||
|
||||
dwg = {
|
||||
'type': 'polygon',
|
||||
'filled': 1,
|
||||
'pos': [0, 0],
|
||||
'polygons': [polygon]
|
||||
}
|
||||
|
||||
if dwg is not None:
|
||||
if el.tag == 'hole' or \
|
||||
el.attrib['layer'] == self.DIMENSION_LAYER:
|
||||
dwg_layer.append(dwg)
|
||||
else:
|
||||
bot = not top
|
||||
|
||||
# Note that in Eagle terminology, 'mirrored'
|
||||
# essentially means 'flipped' (i.e. to the opposite
|
||||
# side of the board)
|
||||
if (mirrored and bot) or (not mirrored and top):
|
||||
dwg_layer['F'].append(dwg)
|
||||
elif (mirrored and top) or (not mirrored and bot):
|
||||
dwg_layer['B'].append(dwg)
|
||||
|
||||
def _name_to_silk(self, name, x, y, elr, tr, align, size, ratio):
|
||||
angle = tr.angle
|
||||
mirrored = tr.mirrored
|
||||
spin = elr.spin ^ tr.spin
|
||||
if mirrored:
|
||||
angle = -angle
|
||||
|
||||
if align is None:
|
||||
justify = [-1, 1]
|
||||
elif align == 'center':
|
||||
justify = [0, 0]
|
||||
else:
|
||||
j = align.split('-')
|
||||
alignments = {
|
||||
'bottom': 1,
|
||||
'center': 0,
|
||||
'top': -1,
|
||||
'left': -1,
|
||||
'right': 1
|
||||
}
|
||||
justify = [alignments[ss] for ss in j[::-1]]
|
||||
if (90 < angle <= 270 and not spin) or \
|
||||
(-90 > angle >= -270 and not spin):
|
||||
angle += 180
|
||||
justify = [-j for j in justify]
|
||||
|
||||
dwg = {
|
||||
'type': 'text',
|
||||
'text': name,
|
||||
'pos': [x, y],
|
||||
'height': size,
|
||||
'width': size,
|
||||
'justify': justify,
|
||||
'thickness': size * ratio,
|
||||
'attr': [] if not mirrored else ['mirrored'],
|
||||
'angle': angle
|
||||
}
|
||||
|
||||
self.font_parser.parse_font_for_string(name)
|
||||
if mirrored:
|
||||
self.pcbdata['drawings']['silkscreen']['B'].append(dwg)
|
||||
else:
|
||||
self.pcbdata['drawings']['silkscreen']['F'].append(dwg)
|
||||
|
||||
def _element_refdes_to_silk(self, el, package):
|
||||
if 'smashed' not in el.attrib:
|
||||
elx = float(el.attrib['x'])
|
||||
ely = -float(el.attrib['y'])
|
||||
for p_el in package.iter('text'):
|
||||
if p_el.text == '>NAME':
|
||||
dx = float(p_el.attrib['x'])
|
||||
dy = float(p_el.attrib['y'])
|
||||
elr = self.Rot(el.get('rot'))
|
||||
dx, dy = self._rotate(dx, dy, elr.angle, elr.mirrored)
|
||||
tr = self.Rot(p_el.get('rot'))
|
||||
tr.angle += elr.angle
|
||||
tr.mirrored ^= elr.mirrored
|
||||
self._name_to_silk(
|
||||
name=el.attrib['name'],
|
||||
x=elx + dx,
|
||||
y=ely - dy,
|
||||
elr=elr,
|
||||
tr=tr,
|
||||
align=p_el.get('align'),
|
||||
size=float(p_el.attrib['size']),
|
||||
ratio=float(p_el.get('ratio', '8')) / 100)
|
||||
|
||||
for attr in el.iter('attribute'):
|
||||
if attr.attrib['name'] == 'NAME':
|
||||
self._name_to_silk(
|
||||
name=el.attrib['name'],
|
||||
x=float(attr.attrib['x']),
|
||||
y=-float(attr.attrib['y']),
|
||||
elr=self.Rot(el.get('rot')),
|
||||
tr=self.Rot(attr.get('rot')),
|
||||
align=attr.attrib.get('align'),
|
||||
size=float(attr.attrib['size']),
|
||||
ratio=float(attr.get('ratio', '8')) / 100)
|
||||
|
||||
@staticmethod
|
||||
def _segments_to_polygon(segs, angle=0, mirrored=False):
|
||||
polygon = []
|
||||
for vertex in segs.iter('vertex'):
|
||||
_x, _y = float(vertex.attrib['x']), -float(vertex.attrib['y'])
|
||||
x, y = FusionEagleParser._rotate(_x, _y, -angle, mirrored)
|
||||
polygon.append([x, y])
|
||||
return polygon
|
||||
|
||||
def _add_zone(self, poly, net):
|
||||
layer = poly.attrib['layer']
|
||||
if layer == self.TOP_COPPER_LAYER:
|
||||
dest = self.pcbdata['zones']['F']
|
||||
elif layer == self.BOT_COPPER_LAYER:
|
||||
dest = self.pcbdata['zones']['B']
|
||||
else:
|
||||
return
|
||||
|
||||
if poly.tag == 'polygonpour':
|
||||
shapes = poly.find('polygonfilldetails').findall('polygonshape')
|
||||
if shapes:
|
||||
zone = {'polygons': [],
|
||||
'fillrule': 'evenodd'}
|
||||
for shape in shapes:
|
||||
segs = shape.find('polygonoutlinesegments')
|
||||
zone['polygons'].append(self._segments_to_polygon(segs))
|
||||
holelist = shape.find('polygonholelist')
|
||||
if holelist:
|
||||
holes = holelist.findall('polygonholesegments')
|
||||
for hole in holes:
|
||||
zone['polygons'].append(self._segments_to_polygon(hole))
|
||||
if self.config.include_nets:
|
||||
zone['net'] = net
|
||||
dest.append(zone)
|
||||
else:
|
||||
zone = {'polygons': []}
|
||||
zone['polygons'].append(self._segments_to_polygon(poly))
|
||||
if self.config.include_nets:
|
||||
zone['net'] = net
|
||||
dest.append(zone)
|
||||
|
||||
def _add_parsed_font_data(self):
|
||||
for (c, wl) in self.font_parser.get_parsed_font().items():
|
||||
if c not in self.pcbdata['font_data']:
|
||||
self.pcbdata['font_data'][c] = wl
|
||||
|
||||
def _parse_param_length(self, name, root, default):
|
||||
# parse named parameter (typically a design rule) assuming it is in
|
||||
# length units (mil or mm)
|
||||
p = [el.attrib['value'] for el in root.iter('param') if
|
||||
el.attrib['name'] == name]
|
||||
if len(p) == 0:
|
||||
self.logger.warning("{0} not found, defaulting to {1}"
|
||||
.format(name, default))
|
||||
return default
|
||||
else:
|
||||
if len(p) > 1:
|
||||
self.logger.warning(
|
||||
"Multiple {0} found, using first occurrence".format(name))
|
||||
p = p[0]
|
||||
p_val = float(''.join(d for d in p if d in string.digits + '.'))
|
||||
p_units = (''.join(d for d in p if d in string.ascii_lowercase))
|
||||
|
||||
if p_units == 'mm':
|
||||
return p_val
|
||||
elif p_units == 'mil':
|
||||
return p_val * 0.0254
|
||||
else:
|
||||
self.logger.error("Unsupported units {0} on {1}"
|
||||
.format(p_units, name))
|
||||
|
||||
def parse(self):
|
||||
ext = os.path.splitext(self.file_name)[1]
|
||||
|
||||
if ext.lower() == '.fbrd':
|
||||
with zipfile.ZipFile(self.file_name) as myzip:
|
||||
brdfilename = [fname for fname in myzip.namelist() if
|
||||
os.path.splitext(fname)[1] == '.brd']
|
||||
with myzip.open(brdfilename[0]) as brdfile:
|
||||
return self._parse(brdfile)
|
||||
|
||||
elif ext.lower() == '.brd':
|
||||
with io.open(self.file_name, 'r', encoding='utf-8') as brdfile:
|
||||
return self._parse(brdfile)
|
||||
|
||||
def _parse(self, brdfile):
|
||||
try:
|
||||
brdxml = ElementTree.parse(brdfile)
|
||||
except ElementTree.ParseError as err:
|
||||
self.logger.error(
|
||||
"Exception occurred trying to parse {0}, message: {1}"
|
||||
.format(brdfile.name, err.msg))
|
||||
return None, None
|
||||
if brdxml is None:
|
||||
self.logger.error(
|
||||
"No data was able to be parsed from {0}".format(brdfile.name))
|
||||
return None, None
|
||||
|
||||
# Pick out key sections
|
||||
root = brdxml.getroot()
|
||||
board = root.find('drawing').find('board')
|
||||
plain = board.find('plain')
|
||||
elements = board.find('elements')
|
||||
signals = board.find('signals')
|
||||
|
||||
# Build library mapping elements' pads to nets
|
||||
self._parse_pad_nets(signals)
|
||||
|
||||
# Parse needed design rules
|
||||
|
||||
# Minimum via annular ring
|
||||
# (Needed in order to calculate through-hole pad diameters correctly)
|
||||
self.min_via_w = (
|
||||
self._parse_param_length('rlMinViaOuter', root, default=0))
|
||||
|
||||
# Minimum drill diameter above which vias will be un-tented
|
||||
self.min_drill_via_untented = (
|
||||
self._parse_param_length('mlViaStopLimit', root, default=0))
|
||||
|
||||
# Signals --> nets
|
||||
if self.config.include_nets:
|
||||
self.pcbdata['nets'] = []
|
||||
for signal in signals.iter('signal'):
|
||||
self.pcbdata['nets'].append(signal.attrib['name'])
|
||||
|
||||
# Signals --> tracks, zones
|
||||
if self.config.include_tracks:
|
||||
self.pcbdata['tracks'] = {'F': [], 'B': []}
|
||||
self.pcbdata['zones'] = {'F': [], 'B': []}
|
||||
for signal in signals.iter('signal'):
|
||||
for wire in signal.iter('wire'):
|
||||
self._add_track(wire, signal.attrib['name'])
|
||||
for via in signal.iter('via'):
|
||||
self._add_track(via, signal.attrib['name'])
|
||||
for poly in signal.iter('polygonpour'):
|
||||
self._add_zone(poly, signal.attrib['name'])
|
||||
|
||||
# Elements --> components, footprints, silkscreen, edges
|
||||
for el in elements.iter('element'):
|
||||
populate = el.get('populate') != 'no'
|
||||
elr = self.Rot(el.get('rot'))
|
||||
layer = 'B' if elr.mirrored else 'F'
|
||||
extra_fields = {}
|
||||
|
||||
for a in el.iter('attribute'):
|
||||
if 'value' in a.attrib:
|
||||
extra_fields[a.attrib['name']] = a.attrib['value']
|
||||
|
||||
comp = Component(ref=el.attrib['name'],
|
||||
val='' if 'value' not in el.attrib else el.attrib[
|
||||
'value'],
|
||||
footprint=el.attrib['package'],
|
||||
layer=layer,
|
||||
attr=None if populate else 'Virtual',
|
||||
extra_fields=extra_fields)
|
||||
|
||||
# For component, get footprint data
|
||||
libs = [lib for lib in board.find('libraries').findall('library')
|
||||
if lib.attrib['name'] == el.attrib['library']]
|
||||
packages = []
|
||||
for lib in libs:
|
||||
p = [pac for pac in lib.find('packages').findall('package')
|
||||
if pac.attrib['name'] == el.attrib['package']]
|
||||
packages.extend(p)
|
||||
if not packages:
|
||||
self.logger.error("Package {0} in library {1} not found in "
|
||||
"source file {2} for element {3}"
|
||||
.format(el.attrib['package'],
|
||||
el.attrib['library'],
|
||||
brdfile.name,
|
||||
el.attrib['name']))
|
||||
return None, None
|
||||
else:
|
||||
package = packages[0]
|
||||
if len(packages) > 1:
|
||||
self.logger.warn("Multiple packages found for package {0}"
|
||||
" in library {1}, using first instance "
|
||||
"found".format(el.attrib['package'],
|
||||
el.attrib['library']))
|
||||
|
||||
elx = float(el.attrib['x'])
|
||||
ely = float(el.attrib['y'])
|
||||
refdes = el.attrib['name']
|
||||
footprint = {
|
||||
'ref': refdes,
|
||||
'center': [elx, ely],
|
||||
'pads': [],
|
||||
'drawings': [],
|
||||
'layer': layer
|
||||
}
|
||||
|
||||
elr = self.Rot(el.get('rot'))
|
||||
footprint['pads'] = self._footprint_pads(package, elx, ely,
|
||||
elr.angle, elr.mirrored,
|
||||
refdes)
|
||||
footprint['bbox'] = self._calculate_footprint_bbox(package, elx,
|
||||
ely, elr.angle,
|
||||
elr.mirrored)
|
||||
self.pcbdata['footprints'].append(footprint)
|
||||
|
||||
# Add silkscreen, edges for component footprint & refdes
|
||||
self._process_footprint(package, elx, ely, elr.angle, elr.mirrored,
|
||||
populate)
|
||||
self._element_refdes_to_silk(el, package)
|
||||
|
||||
self.components.append(comp)
|
||||
|
||||
# Edges & silkscreen (independent of elements)
|
||||
for el in plain.iter():
|
||||
self._add_drawing(el)
|
||||
# identify board bounding box based on edges
|
||||
board_outline_bbox = BoundingBox()
|
||||
|
||||
for drawing in self.pcbdata['edges']:
|
||||
self.add_drawing_bounding_box(drawing, board_outline_bbox)
|
||||
if board_outline_bbox.initialized():
|
||||
self.pcbdata['edges_bbox'] = board_outline_bbox.to_dict()
|
||||
|
||||
self._add_parsed_font_data()
|
||||
|
||||
# Fabrication & metadata
|
||||
company = [a.attrib['value'] for a in root.iter('attribute') if
|
||||
a.attrib['name'] == 'COMPANY']
|
||||
company = '' if not company else company[0]
|
||||
rev = [a.attrib['value'] for a in root.iter('attribute') if
|
||||
a.attrib['name'] == 'REVISION']
|
||||
rev = '' if not rev else rev[0]
|
||||
|
||||
if not rev:
|
||||
rev = ''
|
||||
|
||||
title = os.path.basename(self.file_name)
|
||||
|
||||
variant = [a.attrib['name'] for a in root.iter('variantdef') if
|
||||
a.get('current') == 'yes']
|
||||
variant = None if not variant else variant[0]
|
||||
if variant:
|
||||
title = "{0}, Variant: {1}".format(title, variant)
|
||||
|
||||
date = datetime.fromtimestamp(
|
||||
os.path.getmtime(self.file_name)).strftime('%Y-%m-%d %H:%M:%S')
|
||||
self.pcbdata['metadata'] = {'title': title, 'revision': rev,
|
||||
'company': company, 'date': date}
|
||||
|
||||
return self.pcbdata, self.components
|
Reference in New Issue
Block a user