0321 from macmini
This commit is contained in:
41
3rdparty/plugins/org_openscopeproject_InteractiveHtmlBom/ecad/__init__.py
vendored
Normal file
41
3rdparty/plugins/org_openscopeproject_InteractiveHtmlBom/ecad/__init__.py
vendored
Normal file
@ -0,0 +1,41 @@
|
||||
import os
|
||||
|
||||
|
||||
def get_parser_by_extension(file_name, config, logger):
|
||||
ext = os.path.splitext(file_name)[1]
|
||||
if ext == '.kicad_pcb':
|
||||
return get_kicad_parser(file_name, config, logger)
|
||||
elif ext == '.json':
|
||||
""".json file may be from EasyEDA or a generic json format"""
|
||||
import io
|
||||
import json
|
||||
with io.open(file_name, 'r', encoding='utf-8') as f:
|
||||
obj = json.load(f)
|
||||
if 'pcbdata' in obj:
|
||||
return get_generic_json_parser(file_name, config, logger)
|
||||
else:
|
||||
return get_easyeda_parser(file_name, config, logger)
|
||||
elif ext in ['.fbrd', '.brd']:
|
||||
return get_fusion_eagle_parser(file_name, config, logger)
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def get_kicad_parser(file_name, config, logger, board=None):
|
||||
from .kicad import PcbnewParser
|
||||
return PcbnewParser(file_name, config, logger, board)
|
||||
|
||||
|
||||
def get_easyeda_parser(file_name, config, logger):
|
||||
from .easyeda import EasyEdaParser
|
||||
return EasyEdaParser(file_name, config, logger)
|
||||
|
||||
|
||||
def get_generic_json_parser(file_name, config, logger):
|
||||
from .genericjson import GenericJsonParser
|
||||
return GenericJsonParser(file_name, config, logger)
|
||||
|
||||
|
||||
def get_fusion_eagle_parser(file_name, config, logger):
|
||||
from .fusion_eagle import FusionEagleParser
|
||||
return FusionEagleParser(file_name, config, logger)
|
251
3rdparty/plugins/org_openscopeproject_InteractiveHtmlBom/ecad/common.py
vendored
Normal file
251
3rdparty/plugins/org_openscopeproject_InteractiveHtmlBom/ecad/common.py
vendored
Normal file
@ -0,0 +1,251 @@
|
||||
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
|
493
3rdparty/plugins/org_openscopeproject_InteractiveHtmlBom/ecad/easyeda.py
vendored
Normal file
493
3rdparty/plugins/org_openscopeproject_InteractiveHtmlBom/ecad/easyeda.py
vendored
Normal file
@ -0,0 +1,493 @@
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
|
||||
from .common import EcadParser, Component, BoundingBox, ExtraFieldData
|
||||
|
||||
|
||||
if sys.version_info >= (3, 0):
|
||||
string_types = str
|
||||
else:
|
||||
string_types = basestring # noqa F821: ignore undefined
|
||||
|
||||
|
||||
class EasyEdaParser(EcadParser):
|
||||
TOP_COPPER_LAYER = 1
|
||||
BOT_COPPER_LAYER = 2
|
||||
TOP_SILK_LAYER = 3
|
||||
BOT_SILK_LAYER = 4
|
||||
BOARD_OUTLINE_LAYER = 10
|
||||
TOP_ASSEMBLY_LAYER = 13
|
||||
BOT_ASSEMBLY_LAYER = 14
|
||||
ALL_LAYERS = 11
|
||||
|
||||
def extra_data_file_filter(self):
|
||||
return "Json file ({f})|{f}".format(f=os.path.basename(self.file_name))
|
||||
|
||||
def latest_extra_data(self, extra_dirs=None):
|
||||
return self.file_name
|
||||
|
||||
def get_extra_field_data(self, file_name):
|
||||
if os.path.abspath(file_name) != os.path.abspath(self.file_name):
|
||||
return None
|
||||
|
||||
_, components = self.parse()
|
||||
field_set = set()
|
||||
comp_dict = {}
|
||||
|
||||
for c in components:
|
||||
ref_fields = comp_dict.setdefault(c.ref, {})
|
||||
|
||||
for k, v in c.extra_fields.items():
|
||||
field_set.add(k)
|
||||
ref_fields[k] = v
|
||||
|
||||
by_index = {
|
||||
i: components[i].extra_fields for i in range(len(components))
|
||||
}
|
||||
|
||||
return ExtraFieldData(list(field_set), comp_dict, by_index)
|
||||
|
||||
def get_easyeda_pcb(self):
|
||||
import json
|
||||
with io.open(self.file_name, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
|
||||
@staticmethod
|
||||
def tilda_split(s):
|
||||
# type: (str) -> list
|
||||
return s.split('~')
|
||||
|
||||
@staticmethod
|
||||
def sharp_split(s):
|
||||
# type: (str) -> list
|
||||
return s.split('#@$')
|
||||
|
||||
def _verify(self, pcb):
|
||||
"""Spot check the pcb object."""
|
||||
if 'head' not in pcb:
|
||||
self.logger.error('No head attribute.')
|
||||
return False
|
||||
head = pcb['head']
|
||||
if len(head) < 2:
|
||||
self.logger.error('Incorrect head attribute ' + pcb['head'])
|
||||
return False
|
||||
if head['docType'] != '3':
|
||||
self.logger.error('Incorrect document type: ' + head['docType'])
|
||||
return False
|
||||
if 'canvas' not in pcb:
|
||||
self.logger.error('No canvas attribute.')
|
||||
return False
|
||||
canvas = self.tilda_split(pcb['canvas'])
|
||||
if len(canvas) < 18:
|
||||
self.logger.error('Incorrect canvas attribute ' + pcb['canvas'])
|
||||
return False
|
||||
self.logger.info('EasyEDA editor version ' + head['editorVersion'])
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def normalize(v):
|
||||
if isinstance(v, string_types):
|
||||
v = float(v)
|
||||
return v
|
||||
|
||||
def parse_track(self, shape):
|
||||
shape = self.tilda_split(shape)
|
||||
assert len(shape) >= 5, 'Invalid track ' + str(shape)
|
||||
width = self.normalize(shape[0])
|
||||
layer = int(shape[1])
|
||||
points = [self.normalize(v) for v in shape[3].split(' ')]
|
||||
|
||||
points_xy = [[points[i], points[i + 1]] for i in
|
||||
range(0, len(points), 2)]
|
||||
segments = [(points_xy[i], points_xy[i + 1]) for i in
|
||||
range(len(points_xy) - 1)]
|
||||
segments_json = []
|
||||
for segment in segments:
|
||||
segments_json.append({
|
||||
"type": "segment",
|
||||
"start": segment[0],
|
||||
"end": segment[1],
|
||||
"width": width,
|
||||
})
|
||||
|
||||
return layer, segments_json
|
||||
|
||||
def parse_rect(self, shape):
|
||||
shape = self.tilda_split(shape)
|
||||
assert len(shape) >= 9, 'Invalid rect ' + str(shape)
|
||||
x = self.normalize(shape[0])
|
||||
y = self.normalize(shape[1])
|
||||
width = self.normalize(shape[2])
|
||||
height = self.normalize(shape[3])
|
||||
layer = int(shape[4])
|
||||
fill = shape[8]
|
||||
|
||||
if fill == "none":
|
||||
thickness = self.normalize(shape[7])
|
||||
return layer, [{
|
||||
"type": "rect",
|
||||
"start": [x, y],
|
||||
"end": [x + width, y + height],
|
||||
"width": thickness,
|
||||
}]
|
||||
else:
|
||||
return layer, [{
|
||||
"type": "polygon",
|
||||
"pos": [x, y],
|
||||
"angle": 0,
|
||||
"polygons": [
|
||||
[[0, 0], [width, 0], [width, height], [0, height]]
|
||||
]
|
||||
}]
|
||||
|
||||
def parse_circle(self, shape):
|
||||
shape = self.tilda_split(shape)
|
||||
assert len(shape) >= 6, 'Invalid circle ' + str(shape)
|
||||
cx = self.normalize(shape[0])
|
||||
cy = self.normalize(shape[1])
|
||||
r = self.normalize(shape[2])
|
||||
width = self.normalize(shape[3])
|
||||
layer = int(shape[4])
|
||||
|
||||
return layer, [{
|
||||
"type": "circle",
|
||||
"start": [cx, cy],
|
||||
"radius": r,
|
||||
"width": width
|
||||
}]
|
||||
|
||||
def parse_solid_region(self, shape):
|
||||
shape = self.tilda_split(shape)
|
||||
assert len(shape) >= 5, 'Invalid solid region ' + str(shape)
|
||||
layer = int(shape[0])
|
||||
svgpath = shape[2]
|
||||
|
||||
return layer, [{
|
||||
"type": "polygon",
|
||||
"svgpath": svgpath,
|
||||
}]
|
||||
|
||||
def parse_text(self, shape):
|
||||
shape = self.tilda_split(shape)
|
||||
assert len(shape) >= 12, 'Invalid text ' + str(shape)
|
||||
text_type = shape[0]
|
||||
stroke_width = self.normalize(shape[3])
|
||||
layer = int(shape[6])
|
||||
text = shape[9]
|
||||
svgpath = shape[10]
|
||||
hide = shape[11]
|
||||
|
||||
return layer, [{
|
||||
"type": "text",
|
||||
"text": text,
|
||||
"thickness": stroke_width,
|
||||
"attr": [],
|
||||
"svgpath": svgpath,
|
||||
"hide": hide,
|
||||
"text_type": text_type,
|
||||
}]
|
||||
|
||||
def parse_arc(self, shape):
|
||||
shape = self.tilda_split(shape)
|
||||
assert len(shape) >= 6, 'Invalid arc ' + str(shape)
|
||||
width = self.normalize(shape[0])
|
||||
layer = int(shape[1])
|
||||
svgpath = shape[3]
|
||||
|
||||
return layer, [{
|
||||
"type": "arc",
|
||||
"svgpath": svgpath,
|
||||
"width": width
|
||||
}]
|
||||
|
||||
def parse_hole(self, shape):
|
||||
shape = self.tilda_split(shape)
|
||||
assert len(shape) >= 4, 'Invalid hole ' + str(shape)
|
||||
cx = self.normalize(shape[0])
|
||||
cy = self.normalize(shape[1])
|
||||
radius = self.normalize(shape[2])
|
||||
|
||||
return self.BOARD_OUTLINE_LAYER, [{
|
||||
"type": "circle",
|
||||
"start": [cx, cy],
|
||||
"radius": radius,
|
||||
"width": 0.1, # 1 mil
|
||||
}]
|
||||
|
||||
def parse_pad(self, shape):
|
||||
shape = self.tilda_split(shape)
|
||||
assert len(shape) >= 15, 'Invalid pad ' + str(shape)
|
||||
pad_shape = shape[0]
|
||||
x = self.normalize(shape[1])
|
||||
y = self.normalize(shape[2])
|
||||
width = self.normalize(shape[3])
|
||||
height = self.normalize(shape[4])
|
||||
layer = int(shape[5])
|
||||
number = shape[7]
|
||||
hole_radius = self.normalize(shape[8])
|
||||
if shape[9]:
|
||||
points = [self.normalize(v) for v in shape[9].split(' ')]
|
||||
else:
|
||||
points = []
|
||||
angle = int(shape[10])
|
||||
hole_length = self.normalize(shape[12]) if shape[12] else 0
|
||||
|
||||
pad_layers = {
|
||||
self.TOP_COPPER_LAYER: ['F'],
|
||||
self.BOT_COPPER_LAYER: ['B'],
|
||||
self.ALL_LAYERS: ['F', 'B']
|
||||
}.get(layer)
|
||||
pad_shape = {
|
||||
"ELLIPSE": "circle",
|
||||
"RECT": "rect",
|
||||
"OVAL": "oval",
|
||||
"POLYGON": "custom",
|
||||
}.get(pad_shape)
|
||||
pad_type = "smd" if len(pad_layers) == 1 else "th"
|
||||
|
||||
json = {
|
||||
"layers": pad_layers,
|
||||
"pos": [x, y],
|
||||
"size": [width, height],
|
||||
"angle": angle,
|
||||
"shape": pad_shape,
|
||||
"type": pad_type,
|
||||
}
|
||||
if number == '1':
|
||||
json['pin1'] = 1
|
||||
if pad_shape == "custom":
|
||||
polygon = [(points[i], points[i + 1]) for i in
|
||||
range(0, len(points), 2)]
|
||||
# translate coordinates to be relative to footprint
|
||||
polygon = [(p[0] - x, p[1] - y) for p in polygon]
|
||||
json["polygons"] = [polygon]
|
||||
json["angle"] = 0
|
||||
if pad_type == "th":
|
||||
if hole_length > 1e-6:
|
||||
json["drillshape"] = "oblong"
|
||||
json["drillsize"] = [hole_radius * 2, hole_length]
|
||||
else:
|
||||
json["drillshape"] = "circle"
|
||||
json["drillsize"] = [hole_radius * 2, hole_radius * 2]
|
||||
|
||||
return layer, [{
|
||||
"type": "pad",
|
||||
"pad": json,
|
||||
}]
|
||||
|
||||
@staticmethod
|
||||
def add_pad_bounding_box(pad, bbox):
|
||||
# type: (dict, BoundingBox) -> None
|
||||
|
||||
def add_circle():
|
||||
bbox.add_circle(pad['pos'][0], pad['pos'][1], pad['size'][0] / 2)
|
||||
|
||||
def add_rect():
|
||||
bbox.add_rectangle(pad['pos'][0], pad['pos'][1],
|
||||
pad['size'][0], pad['size'][1],
|
||||
pad['angle'])
|
||||
|
||||
def add_custom():
|
||||
x = pad['pos'][0]
|
||||
y = pad['pos'][1]
|
||||
polygon = pad['polygons'][0]
|
||||
for point in polygon:
|
||||
bbox.add_point(x + point[0], y + point[1])
|
||||
|
||||
{
|
||||
'circle': add_circle,
|
||||
'rect': add_rect,
|
||||
'oval': add_rect,
|
||||
'custom': add_custom,
|
||||
}.get(pad['shape'])()
|
||||
|
||||
def parse_lib(self, shape):
|
||||
parts = self.sharp_split(shape)
|
||||
head = self.tilda_split(parts[0])
|
||||
inner_shapes, _, _ = self.parse_shapes(parts[1:])
|
||||
x = self.normalize(head[0])
|
||||
y = self.normalize(head[1])
|
||||
attr = head[2]
|
||||
fp_layer = int(head[6])
|
||||
|
||||
attr = attr.split('`')
|
||||
if len(attr) % 2 != 0:
|
||||
attr.pop()
|
||||
attr = {attr[i]: attr[i + 1] for i in range(0, len(attr), 2)}
|
||||
fp_layer = 'F' if fp_layer == self.TOP_COPPER_LAYER else 'B'
|
||||
val = '??'
|
||||
ref = '??'
|
||||
footprint = '??'
|
||||
if 'package' in attr:
|
||||
footprint = attr['package']
|
||||
del attr['package']
|
||||
|
||||
pads = []
|
||||
copper_drawings = []
|
||||
extra_drawings = []
|
||||
bbox = BoundingBox()
|
||||
for layer, shapes in inner_shapes.items():
|
||||
for s in shapes:
|
||||
if s["type"] == "pad":
|
||||
pads.append(s["pad"])
|
||||
continue
|
||||
if s["type"] == "text":
|
||||
if s["text_type"] == "N":
|
||||
val = s["text"]
|
||||
if s["text_type"] == "P":
|
||||
ref = s["text"]
|
||||
del s["text_type"]
|
||||
if s["hide"]:
|
||||
continue
|
||||
if layer in [self.TOP_COPPER_LAYER, self.BOT_COPPER_LAYER]:
|
||||
copper_drawings.append({
|
||||
"layer": (
|
||||
'F' if layer == self.TOP_COPPER_LAYER else 'B'),
|
||||
"drawing": s,
|
||||
})
|
||||
elif layer in [self.TOP_SILK_LAYER,
|
||||
self.BOT_SILK_LAYER,
|
||||
self.TOP_ASSEMBLY_LAYER,
|
||||
self.BOT_ASSEMBLY_LAYER,
|
||||
self.BOARD_OUTLINE_LAYER]:
|
||||
extra_drawings.append((layer, s))
|
||||
|
||||
for pad in pads:
|
||||
self.add_pad_bounding_box(pad, bbox)
|
||||
for drawing in copper_drawings:
|
||||
self.add_drawing_bounding_box(drawing['drawing'], bbox)
|
||||
for _, drawing in extra_drawings:
|
||||
self.add_drawing_bounding_box(drawing, bbox)
|
||||
bbox.pad(0.5) # pad by 5 mil
|
||||
if not bbox.initialized():
|
||||
# if bounding box is not calculated yet
|
||||
# set it to 100x100 mil square
|
||||
bbox.add_rectangle(x, y, 10, 10, 0)
|
||||
|
||||
footprint_json = {
|
||||
"ref": ref,
|
||||
"center": [x, y],
|
||||
"bbox": bbox.to_component_dict(),
|
||||
"pads": pads,
|
||||
"drawings": copper_drawings,
|
||||
"layer": fp_layer,
|
||||
}
|
||||
|
||||
component = Component(ref, val, footprint, fp_layer, extra_fields=attr)
|
||||
|
||||
return fp_layer, component, footprint_json, extra_drawings
|
||||
|
||||
def parse_shapes(self, shapes):
|
||||
drawings = {}
|
||||
footprints = []
|
||||
components = []
|
||||
|
||||
for shape_str in shapes:
|
||||
shape = shape_str.split('~', 1)
|
||||
parse_func = {
|
||||
'TRACK': self.parse_track,
|
||||
'RECT': self.parse_rect,
|
||||
'CIRCLE': self.parse_circle,
|
||||
'SOLIDREGION': self.parse_solid_region,
|
||||
'TEXT': self.parse_text,
|
||||
'ARC': self.parse_arc,
|
||||
'PAD': self.parse_pad,
|
||||
'HOLE': self.parse_hole,
|
||||
}.get(shape[0], None)
|
||||
if parse_func:
|
||||
layer, json_list = parse_func(shape[1])
|
||||
drawings.setdefault(layer, []).extend(json_list)
|
||||
if shape[0] == 'LIB':
|
||||
layer, component, json, extras = self.parse_lib(shape[1])
|
||||
for drawing_layer, drawing in extras:
|
||||
drawings.setdefault(drawing_layer, []).append(drawing)
|
||||
footprints.append(json)
|
||||
components.append(component)
|
||||
|
||||
return drawings, footprints, components
|
||||
|
||||
def get_metadata(self, pcb):
|
||||
if hasattr(pcb, 'metadata'):
|
||||
return pcb.metadata
|
||||
else:
|
||||
import os
|
||||
from datetime import datetime
|
||||
pcb_file_name = os.path.basename(self.file_name)
|
||||
title = os.path.splitext(pcb_file_name)[0]
|
||||
file_mtime = os.path.getmtime(self.file_name)
|
||||
file_date = datetime.fromtimestamp(file_mtime).strftime(
|
||||
'%Y-%m-%d %H:%M:%S')
|
||||
return {
|
||||
"title": title,
|
||||
"revision": "",
|
||||
"company": "",
|
||||
"date": file_date,
|
||||
}
|
||||
|
||||
def parse(self):
|
||||
pcb = self.get_easyeda_pcb()
|
||||
if not self._verify(pcb):
|
||||
self.logger.error(
|
||||
'File ' + self.file_name +
|
||||
' does not appear to be valid EasyEDA json file.')
|
||||
return None, None
|
||||
|
||||
drawings, footprints, components = self.parse_shapes(pcb['shape'])
|
||||
|
||||
board_outline_bbox = BoundingBox()
|
||||
for drawing in drawings.get(self.BOARD_OUTLINE_LAYER, []):
|
||||
self.add_drawing_bounding_box(drawing, board_outline_bbox)
|
||||
if board_outline_bbox.initialized():
|
||||
bbox = board_outline_bbox.to_dict()
|
||||
else:
|
||||
# if nothing is drawn on outline layer then rely on EasyEDA bbox
|
||||
x = self.normalize(pcb['BBox']['x'])
|
||||
y = self.normalize(pcb['BBox']['y'])
|
||||
bbox = {
|
||||
"minx": x,
|
||||
"miny": y,
|
||||
"maxx": x + self.normalize(pcb['BBox']['width']),
|
||||
"maxy": y + self.normalize(pcb['BBox']['height'])
|
||||
}
|
||||
|
||||
pcbdata = {
|
||||
"edges_bbox": bbox,
|
||||
"edges": drawings.get(self.BOARD_OUTLINE_LAYER, []),
|
||||
"drawings": {
|
||||
"silkscreen": {
|
||||
'F': drawings.get(self.TOP_SILK_LAYER, []),
|
||||
'B': drawings.get(self.BOT_SILK_LAYER, []),
|
||||
},
|
||||
"fabrication": {
|
||||
'F': drawings.get(self.TOP_ASSEMBLY_LAYER, []),
|
||||
'B': drawings.get(self.BOT_ASSEMBLY_LAYER, []),
|
||||
},
|
||||
},
|
||||
"footprints": footprints,
|
||||
"metadata": self.get_metadata(pcb),
|
||||
"bom": {},
|
||||
"font_data": {}
|
||||
}
|
||||
|
||||
if self.config.include_tracks:
|
||||
def filter_tracks(drawing_list, drawing_type, keys):
|
||||
result = []
|
||||
for d in drawing_list:
|
||||
if d["type"] == drawing_type:
|
||||
r = {}
|
||||
for key in keys:
|
||||
r[key] = d[key]
|
||||
result.append(r)
|
||||
return result
|
||||
|
||||
pcbdata["tracks"] = {
|
||||
'F': filter_tracks(drawings.get(self.TOP_COPPER_LAYER, []),
|
||||
"segment", ["start", "end", "width"]),
|
||||
'B': filter_tracks(drawings.get(self.BOT_COPPER_LAYER, []),
|
||||
"segment", ["start", "end", "width"]),
|
||||
}
|
||||
# zones are not supported
|
||||
pcbdata["zones"] = {'F': [], 'B': []}
|
||||
|
||||
return pcbdata, components
|
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
|
167
3rdparty/plugins/org_openscopeproject_InteractiveHtmlBom/ecad/genericjson.py
vendored
Normal file
167
3rdparty/plugins/org_openscopeproject_InteractiveHtmlBom/ecad/genericjson.py
vendored
Normal file
@ -0,0 +1,167 @@
|
||||
import io
|
||||
import json
|
||||
import os.path
|
||||
from jsonschema import validate, ValidationError
|
||||
|
||||
from .common import EcadParser, Component, BoundingBox, ExtraFieldData
|
||||
from ..core.fontparser import FontParser
|
||||
from ..errors import ParsingException
|
||||
|
||||
|
||||
class GenericJsonParser(EcadParser):
|
||||
COMPATIBLE_SPEC_VERSIONS = [1]
|
||||
|
||||
def extra_data_file_filter(self):
|
||||
return "Json file ({f})|{f}".format(f=os.path.basename(self.file_name))
|
||||
|
||||
def latest_extra_data(self, extra_dirs=None):
|
||||
return self.file_name
|
||||
|
||||
def get_extra_field_data(self, file_name):
|
||||
if os.path.abspath(file_name) != os.path.abspath(self.file_name):
|
||||
return None
|
||||
|
||||
_, components = self._parse()
|
||||
field_set = set()
|
||||
comp_dict = {}
|
||||
|
||||
for c in components:
|
||||
ref_fields = comp_dict.setdefault(c.ref, {})
|
||||
|
||||
for k, v in c.extra_fields.items():
|
||||
field_set.add(k)
|
||||
ref_fields[k] = v
|
||||
|
||||
by_index = {
|
||||
i: components[i].extra_fields for i in range(len(components))
|
||||
}
|
||||
|
||||
return ExtraFieldData(list(field_set), comp_dict, by_index)
|
||||
|
||||
def get_generic_json_pcb(self):
|
||||
with io.open(self.file_name, 'r', encoding='utf-8') as f:
|
||||
pcb = json.load(f)
|
||||
|
||||
if 'spec_version' not in pcb:
|
||||
raise ValidationError("'spec_version' is a required property")
|
||||
|
||||
if pcb['spec_version'] not in self.COMPATIBLE_SPEC_VERSIONS:
|
||||
raise ValidationError("Unsupported spec_version ({})"
|
||||
.format(pcb['spec_version']))
|
||||
|
||||
schema_dir = os.path.join(os.path.dirname(__file__), 'schema')
|
||||
schema_file_name = os.path.join(schema_dir,
|
||||
'genericjsonpcbdata_v{}.schema'.format(
|
||||
pcb['spec_version']))
|
||||
|
||||
with io.open(schema_file_name, 'r', encoding='utf-8') as f:
|
||||
schema = json.load(f)
|
||||
|
||||
validate(instance=pcb, schema=schema)
|
||||
|
||||
return pcb
|
||||
|
||||
def _verify(self, pcb):
|
||||
"""Spot check the pcb object."""
|
||||
|
||||
if len(pcb['pcbdata']['footprints']) != len(pcb['components']):
|
||||
self.logger.error("Length of components list doesn't match"
|
||||
" length of footprints list.")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _texts(pcbdata):
|
||||
for layer in pcbdata['drawings'].values():
|
||||
for side in layer.values():
|
||||
for dwg in side:
|
||||
if 'text' in dwg:
|
||||
yield dwg
|
||||
|
||||
@staticmethod
|
||||
def _remove_control_codes(s):
|
||||
import unicodedata
|
||||
return ''.join(c for c in s if unicodedata.category(c)[0] != "C")
|
||||
|
||||
def _parse_font_data(self, pcbdata):
|
||||
font_parser = FontParser()
|
||||
for dwg in self._texts(pcbdata):
|
||||
if 'svgpath' not in dwg:
|
||||
dwg['text'] = self._remove_control_codes(dwg['text'])
|
||||
font_parser.parse_font_for_string(dwg['text'])
|
||||
|
||||
if font_parser.get_parsed_font():
|
||||
pcbdata['font_data'] = font_parser.get_parsed_font()
|
||||
|
||||
def _check_font_data(self, pcbdata):
|
||||
mc = set()
|
||||
for dwg in self._texts(pcbdata):
|
||||
dwg['text'] = self._remove_control_codes(dwg['text'])
|
||||
mc.update({c for c in dwg['text'] if 'svgpath' not in dwg and
|
||||
c not in pcbdata['font_data']})
|
||||
|
||||
if mc:
|
||||
s = ''.join(mc)
|
||||
self.logger.error('Provided font_data is missing character(s)'
|
||||
f' "{s}" that are present in text drawing'
|
||||
' objects')
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def _parse(self):
|
||||
try:
|
||||
pcb = self.get_generic_json_pcb()
|
||||
except ValidationError as e:
|
||||
self.logger.error('File {f} does not comply with json schema. {m}'
|
||||
.format(f=self.file_name, m=e.message))
|
||||
return None, None
|
||||
|
||||
if not self._verify(pcb):
|
||||
self.logger.error('File {} does not appear to be valid generic'
|
||||
' InteractiveHtmlBom json file.'
|
||||
.format(self.file_name))
|
||||
return None, None
|
||||
|
||||
pcbdata = pcb['pcbdata']
|
||||
components = [Component(**c) for c in pcb['components']]
|
||||
|
||||
if 'font_data' in pcbdata:
|
||||
if not self._check_font_data(pcbdata):
|
||||
raise ParsingException(f'Failed parsing {self.file_name}')
|
||||
else:
|
||||
self._parse_font_data(pcbdata)
|
||||
if 'font_data' in pcbdata:
|
||||
self.logger.info('No font_data provided in JSON, using '
|
||||
'newstroke font')
|
||||
|
||||
self.logger.info('Successfully parsed {}'.format(self.file_name))
|
||||
|
||||
return pcbdata, components
|
||||
|
||||
def parse(self):
|
||||
pcbdata, components = self._parse()
|
||||
|
||||
# override board bounding box based on edges
|
||||
board_outline_bbox = BoundingBox()
|
||||
for drawing in pcbdata['edges']:
|
||||
self.add_drawing_bounding_box(drawing, board_outline_bbox)
|
||||
if board_outline_bbox.initialized():
|
||||
pcbdata['edges_bbox'] = board_outline_bbox.to_dict()
|
||||
|
||||
extra_fields = set(self.config.show_fields)
|
||||
extra_fields.discard("Footprint")
|
||||
extra_fields.discard("Value")
|
||||
if self.config.dnp_field:
|
||||
extra_fields.add(self.config.dnp_field)
|
||||
if self.config.board_variant_field:
|
||||
extra_fields.add(self.config.board_variant_field)
|
||||
if extra_fields:
|
||||
for c in components:
|
||||
c.extra_fields = {
|
||||
f: c.extra_fields.get(f, "") for f in extra_fields}
|
||||
|
||||
self.config.kicad_text_formatting = False
|
||||
|
||||
return pcbdata, components
|
843
3rdparty/plugins/org_openscopeproject_InteractiveHtmlBom/ecad/kicad.py
vendored
Normal file
843
3rdparty/plugins/org_openscopeproject_InteractiveHtmlBom/ecad/kicad.py
vendored
Normal file
@ -0,0 +1,843 @@
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
import pcbnew
|
||||
|
||||
from .common import EcadParser, Component, ExtraFieldData
|
||||
from .kicad_extra import find_latest_schematic_data, parse_schematic_data
|
||||
from .svgpath import create_path
|
||||
from ..core import ibom
|
||||
from ..core.config import Config
|
||||
from ..core.fontparser import FontParser
|
||||
|
||||
|
||||
class PcbnewParser(EcadParser):
|
||||
|
||||
def __init__(self, file_name, config, logger, board=None):
|
||||
super(PcbnewParser, self).__init__(file_name, config, logger)
|
||||
self.board = board
|
||||
if self.board is None:
|
||||
self.board = pcbnew.LoadBoard(self.file_name) # type: pcbnew.BOARD
|
||||
if hasattr(self.board, 'GetModules'):
|
||||
self.footprints = list(self.board.GetModules())
|
||||
else:
|
||||
self.footprints = list(self.board.GetFootprints())
|
||||
self.font_parser = FontParser()
|
||||
|
||||
def get_extra_field_data(self, file_name):
|
||||
if os.path.abspath(file_name) == os.path.abspath(self.file_name):
|
||||
return self.parse_extra_data_from_pcb()
|
||||
if os.path.splitext(file_name)[1] == '.kicad_pcb':
|
||||
return None
|
||||
|
||||
data = parse_schematic_data(file_name)
|
||||
|
||||
return ExtraFieldData(data[0], data[1])
|
||||
|
||||
@staticmethod
|
||||
def get_footprint_fields(f):
|
||||
# type: (pcbnew.FOOTPRINT) -> dict
|
||||
props = {}
|
||||
if hasattr(f, "GetProperties"):
|
||||
props = f.GetProperties()
|
||||
if hasattr(f, "GetFields"):
|
||||
props = f.GetFieldsShownText()
|
||||
if "dnp" in props and props["dnp"] == "":
|
||||
del props["dnp"]
|
||||
props["kicad_dnp"] = "DNP"
|
||||
if hasattr(f, "IsDNP"):
|
||||
if f.IsDNP():
|
||||
props["kicad_dnp"] = "DNP"
|
||||
return props
|
||||
|
||||
def parse_extra_data_from_pcb(self):
|
||||
field_set = set()
|
||||
by_ref = {}
|
||||
by_index = {}
|
||||
|
||||
for (i, f) in enumerate(self.footprints):
|
||||
props = self.get_footprint_fields(f)
|
||||
by_index[i] = props
|
||||
ref = f.GetReference()
|
||||
ref_fields = by_ref.setdefault(ref, {})
|
||||
|
||||
for k, v in props.items():
|
||||
field_set.add(k)
|
||||
ref_fields[k] = v
|
||||
|
||||
return ExtraFieldData(list(field_set), by_ref, by_index)
|
||||
|
||||
def latest_extra_data(self, extra_dirs=None):
|
||||
base_name = os.path.splitext(os.path.basename(self.file_name))[0]
|
||||
extra_dirs.append(self.board.GetPlotOptions().GetOutputDirectory())
|
||||
file_dir_name = os.path.dirname(self.file_name)
|
||||
directories = [file_dir_name]
|
||||
for dir in extra_dirs:
|
||||
if not os.path.isabs(dir):
|
||||
dir = os.path.join(file_dir_name, dir)
|
||||
if os.path.exists(dir):
|
||||
directories.append(dir)
|
||||
return find_latest_schematic_data(base_name, directories)
|
||||
|
||||
def extra_data_file_filter(self):
|
||||
if hasattr(self.board, 'GetModules'):
|
||||
return "Netlist and xml files (*.net; *.xml)|*.net;*.xml"
|
||||
else:
|
||||
return ("Netlist, xml and pcb files (*.net; *.xml; *.kicad_pcb)|"
|
||||
"*.net;*.xml;*.kicad_pcb")
|
||||
|
||||
@staticmethod
|
||||
def normalize(point):
|
||||
return [point.x * 1e-6, point.y * 1e-6]
|
||||
|
||||
@staticmethod
|
||||
def normalize_angle(angle):
|
||||
if isinstance(angle, int) or isinstance(angle, float):
|
||||
return angle * 0.1
|
||||
else:
|
||||
return angle.AsDegrees()
|
||||
|
||||
def get_arc_angles(self, d):
|
||||
# type: (pcbnew.PCB_SHAPE) -> tuple
|
||||
a1 = self.normalize_angle(d.GetArcAngleStart())
|
||||
if hasattr(d, "GetAngle"):
|
||||
a2 = a1 + self.normalize_angle(d.GetAngle())
|
||||
else:
|
||||
a2 = a1 + self.normalize_angle(d.GetArcAngle())
|
||||
if a2 < a1:
|
||||
a1, a2 = a2, a1
|
||||
return round(a1, 2), round(a2, 2)
|
||||
|
||||
def parse_shape(self, d):
|
||||
# type: (pcbnew.PCB_SHAPE) -> dict or None
|
||||
shape = {
|
||||
pcbnew.S_SEGMENT: "segment",
|
||||
pcbnew.S_CIRCLE: "circle",
|
||||
pcbnew.S_ARC: "arc",
|
||||
pcbnew.S_POLYGON: "polygon",
|
||||
pcbnew.S_CURVE: "curve",
|
||||
pcbnew.S_RECT: "rect",
|
||||
}.get(d.GetShape(), "")
|
||||
if shape == "":
|
||||
self.logger.info("Unsupported shape %s, skipping", d.GetShape())
|
||||
return None
|
||||
start = self.normalize(d.GetStart())
|
||||
end = self.normalize(d.GetEnd())
|
||||
if shape == "segment":
|
||||
return {
|
||||
"type": shape,
|
||||
"start": start,
|
||||
"end": end,
|
||||
"width": d.GetWidth() * 1e-6
|
||||
}
|
||||
|
||||
if shape == "rect":
|
||||
if hasattr(d, "GetRectCorners"):
|
||||
points = list(map(self.normalize, d.GetRectCorners()))
|
||||
else:
|
||||
points = [
|
||||
start,
|
||||
[end[0], start[1]],
|
||||
end,
|
||||
[start[0], end[1]]
|
||||
]
|
||||
shape_dict = {
|
||||
"type": "polygon",
|
||||
"pos": [0, 0],
|
||||
"angle": 0,
|
||||
"polygons": [points],
|
||||
"width": d.GetWidth() * 1e-6,
|
||||
"filled": 0
|
||||
}
|
||||
if hasattr(d, "IsFilled") and d.IsFilled():
|
||||
shape_dict["filled"] = 1
|
||||
return shape_dict
|
||||
|
||||
if shape == "circle":
|
||||
shape_dict = {
|
||||
"type": shape,
|
||||
"start": start,
|
||||
"radius": d.GetRadius() * 1e-6,
|
||||
"width": d.GetWidth() * 1e-6
|
||||
}
|
||||
if hasattr(d, "IsFilled") and d.IsFilled():
|
||||
shape_dict["filled"] = 1
|
||||
return shape_dict
|
||||
|
||||
if shape == "arc":
|
||||
a1, a2 = self.get_arc_angles(d)
|
||||
if hasattr(d, "GetCenter"):
|
||||
start = self.normalize(d.GetCenter())
|
||||
return {
|
||||
"type": shape,
|
||||
"start": start,
|
||||
"radius": d.GetRadius() * 1e-6,
|
||||
"startangle": a1,
|
||||
"endangle": a2,
|
||||
"width": d.GetWidth() * 1e-6
|
||||
}
|
||||
|
||||
if shape == "polygon":
|
||||
if hasattr(d, "GetPolyShape"):
|
||||
polygons = self.parse_poly_set(d.GetPolyShape())
|
||||
else:
|
||||
self.logger.info(
|
||||
"Polygons not supported for KiCad 4, skipping")
|
||||
return None
|
||||
angle = 0
|
||||
if hasattr(d, 'GetParentModule'):
|
||||
parent_footprint = d.GetParentModule()
|
||||
else:
|
||||
parent_footprint = d.GetParentFootprint()
|
||||
if parent_footprint is not None:
|
||||
angle = self.normalize_angle(parent_footprint.GetOrientation())
|
||||
shape_dict = {
|
||||
"type": shape,
|
||||
"pos": start,
|
||||
"angle": angle,
|
||||
"polygons": polygons
|
||||
}
|
||||
if hasattr(d, "IsFilled") and not d.IsFilled():
|
||||
shape_dict["filled"] = 0
|
||||
shape_dict["width"] = d.GetWidth() * 1e-6
|
||||
return shape_dict
|
||||
if shape == "curve":
|
||||
if hasattr(d, "GetBezierC1"):
|
||||
c1 = self.normalize(d.GetBezierC1())
|
||||
c2 = self.normalize(d.GetBezierC2())
|
||||
else:
|
||||
c1 = self.normalize(d.GetBezControl1())
|
||||
c2 = self.normalize(d.GetBezControl2())
|
||||
return {
|
||||
"type": shape,
|
||||
"start": start,
|
||||
"cpa": c1,
|
||||
"cpb": c2,
|
||||
"end": end,
|
||||
"width": d.GetWidth() * 1e-6
|
||||
}
|
||||
|
||||
def parse_line_chain(self, shape):
|
||||
# type: (pcbnew.SHAPE_LINE_CHAIN) -> list
|
||||
result = []
|
||||
if not hasattr(shape, "PointCount"):
|
||||
self.logger.warn("No PointCount method on outline object. "
|
||||
"Unpatched kicad version?")
|
||||
return result
|
||||
|
||||
for point_index in range(shape.PointCount()):
|
||||
result.append(
|
||||
self.normalize(shape.CPoint(point_index)))
|
||||
|
||||
return result
|
||||
|
||||
def parse_poly_set(self, poly):
|
||||
# type: (pcbnew.SHAPE_POLY_SET) -> list
|
||||
result = []
|
||||
|
||||
for i in range(poly.OutlineCount()):
|
||||
result.append(self.parse_line_chain(poly.Outline(i)))
|
||||
|
||||
return result
|
||||
|
||||
def parse_text(self, d):
|
||||
# type: (pcbnew.PCB_TEXT) -> dict
|
||||
if not d.IsVisible() and d.GetClass() not in ["PTEXT", "PCB_TEXT"]:
|
||||
return None
|
||||
pos = self.normalize(d.GetPosition())
|
||||
if hasattr(d, "GetTextThickness"):
|
||||
thickness = d.GetTextThickness() * 1e-6
|
||||
else:
|
||||
thickness = d.GetThickness() * 1e-6
|
||||
if hasattr(d, 'TransformToSegmentList'):
|
||||
segments = [self.normalize(p) for p in d.TransformToSegmentList()]
|
||||
lines = []
|
||||
for i in range(0, len(segments), 2):
|
||||
if i == 0 or segments[i - 1] != segments[i]:
|
||||
lines.append([segments[i]])
|
||||
lines[-1].append(segments[i + 1])
|
||||
return {
|
||||
"thickness": thickness,
|
||||
"svgpath": create_path(lines)
|
||||
}
|
||||
elif hasattr(d, 'GetEffectiveTextShape'):
|
||||
shape = d.GetEffectiveTextShape(False) # type: pcbnew.SHAPE_COMPOUND
|
||||
segments = []
|
||||
polygons = []
|
||||
for s in shape.GetSubshapes():
|
||||
if s.Type() == pcbnew.SH_LINE_CHAIN:
|
||||
polygons.append(self.parse_line_chain(s))
|
||||
elif s.Type() == pcbnew.SH_SEGMENT:
|
||||
seg = s.GetSeg()
|
||||
segments.append(
|
||||
[self.normalize(seg.A), self.normalize(seg.B)])
|
||||
else:
|
||||
self.logger.warn(
|
||||
"Unsupported subshape in text: %s" % s.Type())
|
||||
if segments:
|
||||
return {
|
||||
"thickness": thickness,
|
||||
"svgpath": create_path(segments)
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"polygons": polygons
|
||||
}
|
||||
|
||||
if d.GetClass() == "MTEXT":
|
||||
angle = self.normalize_angle(d.GetDrawRotation())
|
||||
else:
|
||||
if hasattr(d, "GetTextAngle"):
|
||||
angle = self.normalize_angle(d.GetTextAngle())
|
||||
else:
|
||||
angle = self.normalize_angle(d.GetOrientation())
|
||||
if hasattr(d, "GetTextHeight"):
|
||||
height = d.GetTextHeight() * 1e-6
|
||||
width = d.GetTextWidth() * 1e-6
|
||||
else:
|
||||
height = d.GetHeight() * 1e-6
|
||||
width = d.GetWidth() * 1e-6
|
||||
if hasattr(d, "GetShownText"):
|
||||
text = d.GetShownText()
|
||||
else:
|
||||
text = d.GetText()
|
||||
self.font_parser.parse_font_for_string(text)
|
||||
attributes = []
|
||||
if d.IsMirrored():
|
||||
attributes.append("mirrored")
|
||||
if d.IsItalic():
|
||||
attributes.append("italic")
|
||||
if d.IsBold():
|
||||
attributes.append("bold")
|
||||
|
||||
return {
|
||||
"pos": pos,
|
||||
"text": text,
|
||||
"height": height,
|
||||
"width": width,
|
||||
"justify": [d.GetHorizJustify(), d.GetVertJustify()],
|
||||
"thickness": thickness,
|
||||
"attr": attributes,
|
||||
"angle": angle
|
||||
}
|
||||
|
||||
def parse_dimension(self, d):
|
||||
# type: (pcbnew.PCB_DIMENSION_BASE) -> dict
|
||||
segments = []
|
||||
circles = []
|
||||
for s in d.GetShapes():
|
||||
s = s.Cast()
|
||||
if s.Type() == pcbnew.SH_SEGMENT:
|
||||
seg = s.GetSeg()
|
||||
segments.append(
|
||||
[self.normalize(seg.A), self.normalize(seg.B)])
|
||||
elif s.Type() == pcbnew.SH_CIRCLE:
|
||||
circles.append(
|
||||
[self.normalize(s.GetCenter()), s.GetRadius() * 1e-6])
|
||||
else:
|
||||
self.logger.info(
|
||||
"Unsupported shape type in dimension object: %s", s.Type())
|
||||
|
||||
svgpath = create_path(segments, circles=circles)
|
||||
|
||||
return {
|
||||
"thickness": d.GetLineThickness() * 1e-6,
|
||||
"svgpath": svgpath
|
||||
}
|
||||
|
||||
def parse_drawing(self, d):
|
||||
# type: (pcbnew.BOARD_ITEM) -> list
|
||||
result = []
|
||||
s = None
|
||||
if d.GetClass() in ["DRAWSEGMENT", "MGRAPHIC", "PCB_SHAPE"]:
|
||||
s = self.parse_shape(d)
|
||||
elif d.GetClass() in ["PTEXT", "MTEXT", "FP_TEXT", "PCB_TEXT", "PCB_FIELD"]:
|
||||
s = self.parse_text(d)
|
||||
elif (d.GetClass().startswith("PCB_DIM")
|
||||
and hasattr(pcbnew, "VECTOR_SHAPEPTR")):
|
||||
result.append(self.parse_dimension(d))
|
||||
if hasattr(d, "Text"):
|
||||
s = self.parse_text(d.Text())
|
||||
else:
|
||||
s = self.parse_text(d)
|
||||
else:
|
||||
self.logger.info("Unsupported drawing class %s, skipping",
|
||||
d.GetClass())
|
||||
if s:
|
||||
result.append(s)
|
||||
return result
|
||||
|
||||
def parse_edges(self, pcb):
|
||||
edges = []
|
||||
drawings = list(pcb.GetDrawings())
|
||||
bbox = None
|
||||
for f in self.footprints:
|
||||
for g in f.GraphicalItems():
|
||||
drawings.append(g)
|
||||
for d in drawings:
|
||||
if d.GetLayer() == pcbnew.Edge_Cuts:
|
||||
for parsed_drawing in self.parse_drawing(d):
|
||||
edges.append(parsed_drawing)
|
||||
if bbox is None:
|
||||
bbox = d.GetBoundingBox()
|
||||
else:
|
||||
bbox.Merge(d.GetBoundingBox())
|
||||
if bbox:
|
||||
bbox.Normalize()
|
||||
return edges, bbox
|
||||
|
||||
def parse_drawings_on_layers(self, drawings, f_layer, b_layer):
|
||||
front = []
|
||||
back = []
|
||||
|
||||
for d in drawings:
|
||||
if d[1].GetLayer() not in [f_layer, b_layer]:
|
||||
continue
|
||||
for drawing in self.parse_drawing(d[1]):
|
||||
if d[0] in ["ref", "val"]:
|
||||
drawing[d[0]] = 1
|
||||
if d[1].GetLayer() == f_layer:
|
||||
front.append(drawing)
|
||||
else:
|
||||
back.append(drawing)
|
||||
|
||||
return {
|
||||
"F": front,
|
||||
"B": back
|
||||
}
|
||||
|
||||
def get_all_drawings(self):
|
||||
drawings = [(d.GetClass(), d) for d in list(self.board.GetDrawings())]
|
||||
for f in self.footprints:
|
||||
drawings.append(("ref", f.Reference()))
|
||||
drawings.append(("val", f.Value()))
|
||||
for d in f.GraphicalItems():
|
||||
drawings.append((d.GetClass(), d))
|
||||
return drawings
|
||||
|
||||
def parse_pad(self, pad):
|
||||
# type: (pcbnew.PAD) -> dict or None
|
||||
layers_set = list(pad.GetLayerSet().Seq())
|
||||
layers = []
|
||||
if pcbnew.F_Cu in layers_set:
|
||||
layers.append("F")
|
||||
if pcbnew.B_Cu in layers_set:
|
||||
layers.append("B")
|
||||
pos = self.normalize(pad.GetPosition())
|
||||
size = self.normalize(pad.GetSize())
|
||||
angle = self.normalize_angle(pad.GetOrientation())
|
||||
shape_lookup = {
|
||||
pcbnew.PAD_SHAPE_RECT: "rect",
|
||||
pcbnew.PAD_SHAPE_OVAL: "oval",
|
||||
pcbnew.PAD_SHAPE_CIRCLE: "circle",
|
||||
}
|
||||
if hasattr(pcbnew, "PAD_SHAPE_TRAPEZOID"):
|
||||
shape_lookup[pcbnew.PAD_SHAPE_TRAPEZOID] = "trapezoid"
|
||||
if hasattr(pcbnew, "PAD_SHAPE_ROUNDRECT"):
|
||||
shape_lookup[pcbnew.PAD_SHAPE_ROUNDRECT] = "roundrect"
|
||||
if hasattr(pcbnew, "PAD_SHAPE_CUSTOM"):
|
||||
shape_lookup[pcbnew.PAD_SHAPE_CUSTOM] = "custom"
|
||||
if hasattr(pcbnew, "PAD_SHAPE_CHAMFERED_RECT"):
|
||||
shape_lookup[pcbnew.PAD_SHAPE_CHAMFERED_RECT] = "chamfrect"
|
||||
shape = shape_lookup.get(pad.GetShape(), "")
|
||||
if shape == "":
|
||||
self.logger.info("Unsupported pad shape %s, skipping.",
|
||||
pad.GetShape())
|
||||
return None
|
||||
pad_dict = {
|
||||
"layers": layers,
|
||||
"pos": pos,
|
||||
"size": size,
|
||||
"angle": angle,
|
||||
"shape": shape
|
||||
}
|
||||
if shape == "custom":
|
||||
polygon_set = pad.GetCustomShapeAsPolygon()
|
||||
if polygon_set.HasHoles():
|
||||
self.logger.warn('Detected holes in custom pad polygons')
|
||||
pad_dict["polygons"] = self.parse_poly_set(polygon_set)
|
||||
if shape == "trapezoid":
|
||||
# treat trapezoid as custom shape
|
||||
pad_dict["shape"] = "custom"
|
||||
delta = self.normalize(pad.GetDelta())
|
||||
pad_dict["polygons"] = [[
|
||||
[size[0] / 2 + delta[1] / 2, size[1] / 2 - delta[0] / 2],
|
||||
[-size[0] / 2 - delta[1] / 2, size[1] / 2 + delta[0] / 2],
|
||||
[-size[0] / 2 + delta[1] / 2, -size[1] / 2 - delta[0] / 2],
|
||||
[size[0] / 2 - delta[1] / 2, -size[1] / 2 + delta[0] / 2],
|
||||
]]
|
||||
|
||||
if shape in ["roundrect", "chamfrect"]:
|
||||
pad_dict["radius"] = pad.GetRoundRectCornerRadius() * 1e-6
|
||||
if shape == "chamfrect":
|
||||
pad_dict["chamfpos"] = pad.GetChamferPositions()
|
||||
pad_dict["chamfratio"] = pad.GetChamferRectRatio()
|
||||
if hasattr(pcbnew, 'PAD_ATTRIB_PTH'):
|
||||
through_hole_attributes = [pcbnew.PAD_ATTRIB_PTH,
|
||||
pcbnew.PAD_ATTRIB_NPTH]
|
||||
else:
|
||||
through_hole_attributes = [pcbnew.PAD_ATTRIB_STANDARD,
|
||||
pcbnew.PAD_ATTRIB_HOLE_NOT_PLATED]
|
||||
if pad.GetAttribute() in through_hole_attributes:
|
||||
pad_dict["type"] = "th"
|
||||
pad_dict["drillshape"] = {
|
||||
pcbnew.PAD_DRILL_SHAPE_CIRCLE: "circle",
|
||||
pcbnew.PAD_DRILL_SHAPE_OBLONG: "oblong"
|
||||
}.get(pad.GetDrillShape())
|
||||
pad_dict["drillsize"] = self.normalize(pad.GetDrillSize())
|
||||
else:
|
||||
pad_dict["type"] = "smd"
|
||||
if hasattr(pad, "GetOffset"):
|
||||
pad_dict["offset"] = self.normalize(pad.GetOffset())
|
||||
if self.config.include_nets:
|
||||
pad_dict["net"] = pad.GetNetname()
|
||||
|
||||
return pad_dict
|
||||
|
||||
def parse_footprints(self):
|
||||
# type: () -> list
|
||||
footprints = []
|
||||
for f in self.footprints: # type: pcbnew.FOOTPRINT
|
||||
ref = f.GetReference()
|
||||
|
||||
# bounding box
|
||||
if hasattr(pcbnew, 'MODULE'):
|
||||
f_copy = pcbnew.MODULE(f)
|
||||
else:
|
||||
f_copy = pcbnew.FOOTPRINT(f)
|
||||
try:
|
||||
f_copy.SetOrientation(0)
|
||||
except TypeError:
|
||||
f_copy.SetOrientation(
|
||||
pcbnew.EDA_ANGLE(0, pcbnew.TENTHS_OF_A_DEGREE_T))
|
||||
pos = f_copy.GetPosition()
|
||||
pos.x = pos.y = 0
|
||||
f_copy.SetPosition(pos)
|
||||
if hasattr(f_copy, 'GetFootprintRect'):
|
||||
footprint_rect = f_copy.GetFootprintRect()
|
||||
else:
|
||||
footprint_rect = f_copy.GetBoundingBox(False, False)
|
||||
bbox = {
|
||||
"pos": self.normalize(f.GetPosition()),
|
||||
"relpos": self.normalize(footprint_rect.GetPosition()),
|
||||
"size": self.normalize(footprint_rect.GetSize()),
|
||||
"angle": self.normalize_angle(f.GetOrientation()),
|
||||
}
|
||||
|
||||
# graphical drawings
|
||||
drawings = []
|
||||
for d in f.GraphicalItems():
|
||||
# we only care about copper ones, silkscreen is taken care of
|
||||
if d.GetLayer() not in [pcbnew.F_Cu, pcbnew.B_Cu]:
|
||||
continue
|
||||
for drawing in self.parse_drawing(d):
|
||||
drawings.append({
|
||||
"layer": "F" if d.GetLayer() == pcbnew.F_Cu else "B",
|
||||
"drawing": drawing,
|
||||
})
|
||||
|
||||
# footprint pads
|
||||
pads = []
|
||||
for p in f.Pads():
|
||||
pad_dict = self.parse_pad(p)
|
||||
if pad_dict is not None:
|
||||
pads.append((p.GetPadName(), pad_dict))
|
||||
|
||||
if pads:
|
||||
# Try to guess first pin name.
|
||||
pads = sorted(pads, key=lambda el: el[0])
|
||||
pin1_pads = [p for p in pads if p[0] in
|
||||
['1', 'A', 'A1', 'P1', 'PAD1']]
|
||||
if pin1_pads:
|
||||
pin1_pad_name = pin1_pads[0][0]
|
||||
else:
|
||||
# No pads have common first pin name,
|
||||
# pick lexicographically smallest.
|
||||
pin1_pad_name = pads[0][0]
|
||||
for pad_name, pad_dict in pads:
|
||||
if pad_name == pin1_pad_name:
|
||||
pad_dict['pin1'] = 1
|
||||
|
||||
pads = [p[1] for p in pads]
|
||||
|
||||
# add footprint
|
||||
footprints.append({
|
||||
"ref": ref,
|
||||
"bbox": bbox,
|
||||
"pads": pads,
|
||||
"drawings": drawings,
|
||||
"layer": {
|
||||
pcbnew.F_Cu: "F",
|
||||
pcbnew.B_Cu: "B"
|
||||
}.get(f.GetLayer())
|
||||
})
|
||||
|
||||
return footprints
|
||||
|
||||
def parse_tracks(self, tracks):
|
||||
tent_vias = True
|
||||
if hasattr(self.board, "GetTentVias"):
|
||||
tent_vias = self.board.GetTentVias()
|
||||
result = {pcbnew.F_Cu: [], pcbnew.B_Cu: []}
|
||||
for track in tracks:
|
||||
if track.GetClass() in ["VIA", "PCB_VIA"]:
|
||||
track_dict = {
|
||||
"start": self.normalize(track.GetStart()),
|
||||
"end": self.normalize(track.GetEnd()),
|
||||
"width": track.GetWidth() * 1e-6,
|
||||
"net": track.GetNetname(),
|
||||
}
|
||||
if not tent_vias:
|
||||
track_dict["drillsize"] = track.GetDrillValue() * 1e-6
|
||||
for layer in [pcbnew.F_Cu, pcbnew.B_Cu]:
|
||||
if track.IsOnLayer(layer):
|
||||
result[layer].append(track_dict)
|
||||
else:
|
||||
if track.GetLayer() in [pcbnew.F_Cu, pcbnew.B_Cu]:
|
||||
if track.GetClass() in ["ARC", "PCB_ARC"]:
|
||||
a1, a2 = self.get_arc_angles(track)
|
||||
track_dict = {
|
||||
"center": self.normalize(track.GetCenter()),
|
||||
"startangle": a1,
|
||||
"endangle": a2,
|
||||
"radius": track.GetRadius() * 1e-6,
|
||||
"width": track.GetWidth() * 1e-6,
|
||||
}
|
||||
else:
|
||||
track_dict = {
|
||||
"start": self.normalize(track.GetStart()),
|
||||
"end": self.normalize(track.GetEnd()),
|
||||
"width": track.GetWidth() * 1e-6,
|
||||
}
|
||||
if self.config.include_nets:
|
||||
track_dict["net"] = track.GetNetname()
|
||||
result[track.GetLayer()].append(track_dict)
|
||||
|
||||
return {
|
||||
'F': result.get(pcbnew.F_Cu),
|
||||
'B': result.get(pcbnew.B_Cu)
|
||||
}
|
||||
|
||||
def parse_zones(self, zones):
|
||||
result = {pcbnew.F_Cu: [], pcbnew.B_Cu: []}
|
||||
for zone in zones: # type: pcbnew.ZONE
|
||||
if (not zone.IsFilled() or
|
||||
hasattr(zone, 'GetIsKeepout') and zone.GetIsKeepout() or
|
||||
hasattr(zone, 'GetIsRuleArea') and zone.GetIsRuleArea()):
|
||||
continue
|
||||
layers = [layer for layer in list(zone.GetLayerSet().Seq())
|
||||
if layer in [pcbnew.F_Cu, pcbnew.B_Cu]]
|
||||
for layer in layers:
|
||||
try:
|
||||
# kicad 5.1 and earlier
|
||||
poly_set = zone.GetFilledPolysList()
|
||||
except TypeError:
|
||||
poly_set = zone.GetFilledPolysList(layer)
|
||||
width = zone.GetMinThickness() * 1e-6
|
||||
if (hasattr(zone, 'GetFilledPolysUseThickness') and
|
||||
not zone.GetFilledPolysUseThickness()):
|
||||
width = 0
|
||||
zone_dict = {
|
||||
"polygons": self.parse_poly_set(poly_set),
|
||||
"width": width,
|
||||
}
|
||||
if self.config.include_nets:
|
||||
zone_dict["net"] = zone.GetNetname()
|
||||
result[layer].append(zone_dict)
|
||||
|
||||
return {
|
||||
'F': result.get(pcbnew.F_Cu),
|
||||
'B': result.get(pcbnew.B_Cu)
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def parse_netlist(net_info):
|
||||
# type: (pcbnew.NETINFO_LIST) -> list
|
||||
nets = net_info.NetsByName().asdict().keys()
|
||||
nets = sorted([str(s) for s in nets])
|
||||
return nets
|
||||
|
||||
@staticmethod
|
||||
def footprint_to_component(footprint, extra_fields):
|
||||
try:
|
||||
footprint_name = str(footprint.GetFPID().GetFootprintName())
|
||||
except AttributeError:
|
||||
footprint_name = str(footprint.GetFPID().GetLibItemName())
|
||||
|
||||
attr = 'Normal'
|
||||
if hasattr(pcbnew, 'FP_EXCLUDE_FROM_BOM'):
|
||||
if footprint.GetAttributes() & pcbnew.FP_EXCLUDE_FROM_BOM:
|
||||
attr = 'Virtual'
|
||||
elif hasattr(pcbnew, 'MOD_VIRTUAL'):
|
||||
if footprint.GetAttributes() == pcbnew.MOD_VIRTUAL:
|
||||
attr = 'Virtual'
|
||||
layer = {
|
||||
pcbnew.F_Cu: 'F',
|
||||
pcbnew.B_Cu: 'B',
|
||||
}.get(footprint.GetLayer())
|
||||
|
||||
return Component(footprint.GetReference(),
|
||||
footprint.GetValue(),
|
||||
footprint_name,
|
||||
layer,
|
||||
attr,
|
||||
extra_fields)
|
||||
|
||||
def parse(self):
|
||||
from ..errors import ParsingException
|
||||
|
||||
# Get extra field data from netlist
|
||||
field_set = set(self.config.show_fields)
|
||||
field_set.discard("Value")
|
||||
field_set.discard("Footprint")
|
||||
need_extra_fields = (field_set or
|
||||
self.config.board_variant_whitelist or
|
||||
self.config.board_variant_blacklist or
|
||||
self.config.dnp_field)
|
||||
|
||||
if not self.config.extra_data_file and need_extra_fields:
|
||||
self.logger.warn('Ignoring extra fields related config parameters '
|
||||
'since no netlist/xml file was specified.')
|
||||
need_extra_fields = False
|
||||
|
||||
extra_field_data = None
|
||||
if (self.config.extra_data_file and
|
||||
os.path.isfile(self.config.extra_data_file)):
|
||||
extra_field_data = self.parse_extra_data(
|
||||
self.config.extra_data_file, self.config.normalize_field_case)
|
||||
|
||||
if extra_field_data is None and need_extra_fields:
|
||||
raise ParsingException(
|
||||
'Failed parsing %s' % self.config.extra_data_file)
|
||||
|
||||
title_block = self.board.GetTitleBlock()
|
||||
title = title_block.GetTitle()
|
||||
revision = title_block.GetRevision()
|
||||
company = title_block.GetCompany()
|
||||
file_date = title_block.GetDate()
|
||||
if (hasattr(self.board, "GetProject") and
|
||||
hasattr(pcbnew, "ExpandTextVars")):
|
||||
project = self.board.GetProject()
|
||||
title = pcbnew.ExpandTextVars(title, project)
|
||||
revision = pcbnew.ExpandTextVars(revision, project)
|
||||
company = pcbnew.ExpandTextVars(company, project)
|
||||
file_date = pcbnew.ExpandTextVars(file_date, project)
|
||||
|
||||
if not file_date:
|
||||
file_mtime = os.path.getmtime(self.file_name)
|
||||
file_date = datetime.fromtimestamp(file_mtime).strftime(
|
||||
'%Y-%m-%d %H:%M:%S')
|
||||
pcb_file_name = os.path.basename(self.file_name)
|
||||
if not title:
|
||||
# remove .kicad_pcb extension
|
||||
title = os.path.splitext(pcb_file_name)[0]
|
||||
edges, bbox = self.parse_edges(self.board)
|
||||
if bbox is None:
|
||||
self.logger.error('Please draw pcb outline on the edges '
|
||||
'layer on sheet or any footprint before '
|
||||
'generating BOM.')
|
||||
return None, None
|
||||
bbox = {
|
||||
"minx": bbox.GetPosition().x * 1e-6,
|
||||
"miny": bbox.GetPosition().y * 1e-6,
|
||||
"maxx": bbox.GetRight() * 1e-6,
|
||||
"maxy": bbox.GetBottom() * 1e-6,
|
||||
}
|
||||
|
||||
drawings = self.get_all_drawings()
|
||||
|
||||
pcbdata = {
|
||||
"edges_bbox": bbox,
|
||||
"edges": edges,
|
||||
"drawings": {
|
||||
"silkscreen": self.parse_drawings_on_layers(
|
||||
drawings, pcbnew.F_SilkS, pcbnew.B_SilkS),
|
||||
"fabrication": self.parse_drawings_on_layers(
|
||||
drawings, pcbnew.F_Fab, pcbnew.B_Fab),
|
||||
},
|
||||
"footprints": self.parse_footprints(),
|
||||
"metadata": {
|
||||
"title": title,
|
||||
"revision": revision,
|
||||
"company": company,
|
||||
"date": file_date,
|
||||
},
|
||||
"bom": {},
|
||||
"font_data": self.font_parser.get_parsed_font()
|
||||
}
|
||||
if self.config.include_tracks:
|
||||
pcbdata["tracks"] = self.parse_tracks(self.board.GetTracks())
|
||||
if hasattr(self.board, "Zones"):
|
||||
pcbdata["zones"] = self.parse_zones(self.board.Zones())
|
||||
else:
|
||||
self.logger.info("Zones not supported for KiCad 4, skipping")
|
||||
pcbdata["zones"] = {'F': [], 'B': []}
|
||||
if self.config.include_nets and hasattr(self.board, "GetNetInfo"):
|
||||
pcbdata["nets"] = self.parse_netlist(self.board.GetNetInfo())
|
||||
|
||||
if extra_field_data and need_extra_fields:
|
||||
extra_fields = extra_field_data.fields_by_index
|
||||
if extra_fields:
|
||||
extra_fields = extra_fields.values()
|
||||
|
||||
if extra_fields is None:
|
||||
extra_fields = []
|
||||
field_map = extra_field_data.fields_by_ref
|
||||
warning_shown = False
|
||||
|
||||
for f in self.footprints:
|
||||
extra_fields.append(field_map.get(f.GetReference(), {}))
|
||||
if f.GetReference() not in field_map:
|
||||
# Some components are on pcb but not in schematic data.
|
||||
# Show a warning about outdated extra data file.
|
||||
self.logger.warn(
|
||||
'Component %s is missing from schematic data.'
|
||||
% f.GetReference())
|
||||
warning_shown = True
|
||||
|
||||
if warning_shown:
|
||||
self.logger.warn('Netlist/xml file is likely out of date.')
|
||||
else:
|
||||
extra_fields = [{}] * len(self.footprints)
|
||||
|
||||
components = [self.footprint_to_component(f, e)
|
||||
for (f, e) in zip(self.footprints, extra_fields)]
|
||||
|
||||
return pcbdata, components
|
||||
|
||||
|
||||
class InteractiveHtmlBomPlugin(pcbnew.ActionPlugin, object):
|
||||
|
||||
def __init__(self):
|
||||
super(InteractiveHtmlBomPlugin, self).__init__()
|
||||
self.name = "Generate Interactive HTML BOM"
|
||||
self.category = "Read PCB"
|
||||
self.pcbnew_icon_support = hasattr(self, "show_toolbar_button")
|
||||
self.show_toolbar_button = True
|
||||
icon_dir = os.path.dirname(os.path.dirname(__file__))
|
||||
self.icon_file_name = os.path.join(icon_dir, 'icon.png')
|
||||
self.description = "Generate interactive HTML page with BOM " \
|
||||
"table and pcb drawing."
|
||||
|
||||
def defaults(self):
|
||||
pass
|
||||
|
||||
def Run(self):
|
||||
from ..version import version
|
||||
from ..errors import ParsingException
|
||||
|
||||
logger = ibom.Logger()
|
||||
board = pcbnew.GetBoard()
|
||||
pcb_file_name = board.GetFileName()
|
||||
|
||||
if not pcb_file_name:
|
||||
logger.error('Please save the board file before generating BOM.')
|
||||
return
|
||||
|
||||
config = Config(version, os.path.dirname(pcb_file_name))
|
||||
parser = PcbnewParser(pcb_file_name, config, logger, board)
|
||||
|
||||
try:
|
||||
ibom.run_with_dialog(parser, config, logger)
|
||||
except ParsingException as e:
|
||||
logger.error(str(e))
|
59
3rdparty/plugins/org_openscopeproject_InteractiveHtmlBom/ecad/kicad_extra/__init__.py
vendored
Normal file
59
3rdparty/plugins/org_openscopeproject_InteractiveHtmlBom/ecad/kicad_extra/__init__.py
vendored
Normal file
@ -0,0 +1,59 @@
|
||||
import os
|
||||
import pcbnew
|
||||
|
||||
from .xmlparser import XmlParser
|
||||
from .netlistparser import NetlistParser
|
||||
|
||||
PARSERS = {
|
||||
'.xml': XmlParser,
|
||||
'.net': NetlistParser,
|
||||
}
|
||||
|
||||
|
||||
if hasattr(pcbnew, 'FOOTPRINT'):
|
||||
PARSERS['.kicad_pcb'] = None
|
||||
|
||||
|
||||
def parse_schematic_data(file_name):
|
||||
if not os.path.isfile(file_name):
|
||||
return None
|
||||
extension = os.path.splitext(file_name)[1]
|
||||
if extension not in PARSERS:
|
||||
return None
|
||||
else:
|
||||
parser_cls = PARSERS[extension]
|
||||
if parser_cls is None:
|
||||
return None
|
||||
parser = parser_cls(file_name)
|
||||
return parser.get_extra_field_data()
|
||||
|
||||
|
||||
def find_latest_schematic_data(base_name, directories):
|
||||
"""
|
||||
:param base_name: base name of pcb file
|
||||
:param directories: list of directories to search
|
||||
:return: last modified parsable file path or None if not found
|
||||
"""
|
||||
files = []
|
||||
for d in directories:
|
||||
files.extend(_find_in_dir(d))
|
||||
# sort by decreasing modification time
|
||||
files = sorted(files, reverse=True)
|
||||
if files:
|
||||
# try to find first (last modified) file that has name matching pcb file
|
||||
for _, f in files:
|
||||
if os.path.splitext(os.path.basename(f))[0] == base_name:
|
||||
return f
|
||||
# if no such file is found just return last modified
|
||||
return files[0][1]
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def _find_in_dir(dir):
|
||||
_, _, files = next(os.walk(dir), (None, None, []))
|
||||
# filter out files that we can not parse
|
||||
files = [f for f in files if os.path.splitext(f)[1] in PARSERS.keys()]
|
||||
files = [os.path.join(dir, f) for f in files]
|
||||
# get their modification time and sort in descending order
|
||||
return [(os.path.getmtime(f), f) for f in files]
|
59
3rdparty/plugins/org_openscopeproject_InteractiveHtmlBom/ecad/kicad_extra/netlistparser.py
vendored
Normal file
59
3rdparty/plugins/org_openscopeproject_InteractiveHtmlBom/ecad/kicad_extra/netlistparser.py
vendored
Normal file
@ -0,0 +1,59 @@
|
||||
import io
|
||||
|
||||
from .parser_base import ParserBase
|
||||
from .sexpressions import parse_sexpression
|
||||
|
||||
|
||||
class NetlistParser(ParserBase):
|
||||
def get_extra_field_data(self):
|
||||
with io.open(self.file_name, 'r', encoding='utf-8') as f:
|
||||
sexpression = parse_sexpression(f.read())
|
||||
components = None
|
||||
for s in sexpression:
|
||||
if s[0] == 'components':
|
||||
components = s[1:]
|
||||
if components is None:
|
||||
return None
|
||||
field_set = set()
|
||||
comp_dict = {}
|
||||
for c in components:
|
||||
ref = None
|
||||
fields = None
|
||||
datasheet = None
|
||||
libsource = None
|
||||
dnp = False
|
||||
for f in c[1:]:
|
||||
if f[0] == 'ref':
|
||||
ref = f[1]
|
||||
if f[0] == 'fields':
|
||||
fields = f[1:]
|
||||
if f[0] == 'datasheet':
|
||||
datasheet = f[1]
|
||||
if f[0] == 'libsource':
|
||||
libsource = f[1:]
|
||||
if f[0] == 'property' and isinstance(f[1], list) and \
|
||||
f[1][0] == 'name' and f[1][1] == 'dnp':
|
||||
dnp = True
|
||||
if ref is None:
|
||||
return None
|
||||
ref_fields = comp_dict.setdefault(ref, {})
|
||||
if datasheet and datasheet != '~':
|
||||
field_set.add('Datasheet')
|
||||
ref_fields['Datasheet'] = datasheet
|
||||
if libsource is not None:
|
||||
for lib_field in libsource:
|
||||
if lib_field[0] == 'description':
|
||||
field_set.add('Description')
|
||||
ref_fields['Description'] = lib_field[1]
|
||||
if dnp:
|
||||
field_set.add('kicad_dnp')
|
||||
ref_fields['kicad_dnp'] = "DNP"
|
||||
if fields is None:
|
||||
continue
|
||||
for f in fields:
|
||||
if len(f) > 1:
|
||||
field_set.add(f[1][1])
|
||||
if len(f) > 2:
|
||||
ref_fields[f[1][1]] = f[2]
|
||||
|
||||
return list(field_set), comp_dict
|
26
3rdparty/plugins/org_openscopeproject_InteractiveHtmlBom/ecad/kicad_extra/parser_base.py
vendored
Normal file
26
3rdparty/plugins/org_openscopeproject_InteractiveHtmlBom/ecad/kicad_extra/parser_base.py
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
class ParserBase:
|
||||
|
||||
def __init__(self, file_name):
|
||||
"""
|
||||
:param file_name: path to file that should be parsed.
|
||||
"""
|
||||
self.file_name = file_name
|
||||
|
||||
def get_extra_field_data(self):
|
||||
# type: () -> tuple
|
||||
"""
|
||||
Parses the file and returns extra field data.
|
||||
:return: tuple of the format
|
||||
(
|
||||
[field_name1, field_name2,... ],
|
||||
{
|
||||
ref1: {
|
||||
field_name1: field_value1,
|
||||
field_name2: field_value2,
|
||||
...
|
||||
],
|
||||
ref2: ...
|
||||
}
|
||||
)
|
||||
"""
|
||||
pass
|
32
3rdparty/plugins/org_openscopeproject_InteractiveHtmlBom/ecad/kicad_extra/sexpressions.py
vendored
Normal file
32
3rdparty/plugins/org_openscopeproject_InteractiveHtmlBom/ecad/kicad_extra/sexpressions.py
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
import re
|
||||
|
||||
term_regex = r'''(?mx)
|
||||
\s*(?:
|
||||
(?P<open>\()|
|
||||
(?P<close>\))|
|
||||
(?P<sq>"(?:\\\\|\\"|[^"])*")|
|
||||
(?P<s>[^(^)\s]+)
|
||||
)'''
|
||||
pattern = re.compile(term_regex)
|
||||
|
||||
|
||||
def parse_sexpression(sexpression):
|
||||
stack = []
|
||||
out = []
|
||||
for terms in pattern.finditer(sexpression):
|
||||
term, value = [(t, v) for t, v in terms.groupdict().items() if v][0]
|
||||
if term == 'open':
|
||||
stack.append(out)
|
||||
out = []
|
||||
elif term == 'close':
|
||||
assert stack, "Trouble with nesting of brackets"
|
||||
tmp, out = out, stack.pop(-1)
|
||||
out.append(tmp)
|
||||
elif term == 'sq':
|
||||
out.append(value[1:-1].replace('\\\\', '\\').replace('\\"', '"'))
|
||||
elif term == 's':
|
||||
out.append(value)
|
||||
else:
|
||||
raise NotImplementedError("Error: %s, %s" % (term, value))
|
||||
assert not stack, "Trouble with nesting of brackets"
|
||||
return out[0]
|
42
3rdparty/plugins/org_openscopeproject_InteractiveHtmlBom/ecad/kicad_extra/xmlparser.py
vendored
Normal file
42
3rdparty/plugins/org_openscopeproject_InteractiveHtmlBom/ecad/kicad_extra/xmlparser.py
vendored
Normal file
@ -0,0 +1,42 @@
|
||||
from xml.dom import minidom
|
||||
|
||||
from .parser_base import ParserBase
|
||||
|
||||
|
||||
class XmlParser(ParserBase):
|
||||
@staticmethod
|
||||
def get_text(nodelist):
|
||||
rc = []
|
||||
for node in nodelist:
|
||||
if node.nodeType == node.TEXT_NODE:
|
||||
rc.append(node.data)
|
||||
return ''.join(rc)
|
||||
|
||||
def get_extra_field_data(self):
|
||||
xml = minidom.parse(self.file_name)
|
||||
components = xml.getElementsByTagName('comp')
|
||||
field_set = set()
|
||||
comp_dict = {}
|
||||
for c in components:
|
||||
ref_fields = comp_dict.setdefault(c.attributes['ref'].value, {})
|
||||
datasheet = c.getElementsByTagName('datasheet')
|
||||
if datasheet:
|
||||
datasheet = self.get_text(datasheet[0].childNodes)
|
||||
if datasheet != '~':
|
||||
field_set.add('Datasheet')
|
||||
ref_fields['Datasheet'] = datasheet
|
||||
libsource = c.getElementsByTagName('libsource')
|
||||
if libsource and libsource[0].hasAttribute('description'):
|
||||
field_set.add('Description')
|
||||
attr = libsource[0].attributes['description']
|
||||
ref_fields['Description'] = attr.value
|
||||
for f in c.getElementsByTagName('field'):
|
||||
name = f.attributes['name'].value
|
||||
field_set.add(name)
|
||||
ref_fields[name] = self.get_text(f.childNodes)
|
||||
for f in c.getElementsByTagName('property'):
|
||||
if f.attributes['name'].value == 'dnp':
|
||||
field_set.add('kicad_dnp')
|
||||
ref_fields['kicad_dnp'] = "DNP"
|
||||
|
||||
return list(field_set), comp_dict
|
638
3rdparty/plugins/org_openscopeproject_InteractiveHtmlBom/ecad/schema/genericjsonpcbdata_v1.schema
vendored
Normal file
638
3rdparty/plugins/org_openscopeproject_InteractiveHtmlBom/ecad/schema/genericjsonpcbdata_v1.schema
vendored
Normal file
@ -0,0 +1,638 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-06/schema#",
|
||||
"$ref": "#/definitions/GenericJSONPCBData",
|
||||
"definitions": {
|
||||
"GenericJSONPCBData": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"spec_version": {
|
||||
"type": "integer"
|
||||
},
|
||||
"pcbdata": {
|
||||
"$ref": "#/definitions/Pcbdata"
|
||||
},
|
||||
"components": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Component"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"spec_version",
|
||||
"pcbdata",
|
||||
"components"
|
||||
],
|
||||
"title": "GenericJSONPCBData"
|
||||
},
|
||||
"Component": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"attr": {
|
||||
"type": "string"
|
||||
},
|
||||
"footprint": {
|
||||
"type": "string"
|
||||
},
|
||||
"layer": {
|
||||
"$ref": "#/definitions/Layer"
|
||||
},
|
||||
"ref": {
|
||||
"type": "string"
|
||||
},
|
||||
"val": {
|
||||
"type": "string"
|
||||
},
|
||||
"extra_fields": {
|
||||
"$ref": "#/definitions/ExtraData"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"footprint",
|
||||
"layer",
|
||||
"ref",
|
||||
"val"
|
||||
],
|
||||
"title": "Component"
|
||||
},
|
||||
"Pcbdata": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"edges_bbox": {
|
||||
"$ref": "#/definitions/EdgesBbox"
|
||||
},
|
||||
"edges": {
|
||||
"$ref": "#/definitions/DrawingArray"
|
||||
},
|
||||
"drawings": {
|
||||
"$ref": "#/definitions/LayerDrawings"
|
||||
},
|
||||
"footprints": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Footprint"
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
"$ref": "#/definitions/Metadata"
|
||||
},
|
||||
"tracks": {
|
||||
"$ref": "#/definitions/Tracks"
|
||||
},
|
||||
"zones": {
|
||||
"$ref": "#/definitions/Zones"
|
||||
},
|
||||
"nets": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"font_data": {
|
||||
"$ref": "#/definitions/FontData"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"edges_bbox",
|
||||
"edges",
|
||||
"drawings",
|
||||
"footprints",
|
||||
"metadata"
|
||||
],
|
||||
"dependencies": {
|
||||
"tracks": { "required": ["zones"] },
|
||||
"zones": { "required": ["tracks"] }
|
||||
},
|
||||
"title": "Pcbdata"
|
||||
},
|
||||
"EdgesBbox": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"minx": {
|
||||
"type": "number"
|
||||
},
|
||||
"miny": {
|
||||
"type": "number"
|
||||
},
|
||||
"maxx": {
|
||||
"type": "number"
|
||||
},
|
||||
"maxy": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": ["minx", "miny", "maxx", "maxy"],
|
||||
"title": "EdgesBbox"
|
||||
},
|
||||
"DrawingSet": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"F": {
|
||||
"$ref": "#/definitions/DrawingArray"
|
||||
},
|
||||
"B": {
|
||||
"$ref": "#/definitions/DrawingArray"
|
||||
}
|
||||
},
|
||||
"required": ["F", "B"],
|
||||
"title": "DrawingSet"
|
||||
},
|
||||
"Footprint": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"ref": {
|
||||
"type": "string"
|
||||
},
|
||||
"center": {
|
||||
"$ref": "#/definitions/Coordinates"
|
||||
},
|
||||
"bbox": {
|
||||
"$ref": "#/definitions/Bbox"
|
||||
},
|
||||
"pads": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Pad"
|
||||
}
|
||||
},
|
||||
"drawings": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"layer": { "$ref": "#/definitions/Layer" },
|
||||
"drawing": { "$ref": "#/definitions/Drawing" }
|
||||
},
|
||||
"required": [ "layer", "drawing" ]
|
||||
}
|
||||
},
|
||||
"layer": {
|
||||
"$ref": "#/definitions/Layer"
|
||||
}
|
||||
},
|
||||
"required": ["ref", "center", "bbox", "pads", "drawings", "layer"],
|
||||
"title": "Footprint"
|
||||
},
|
||||
"Bbox": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"pos": {
|
||||
"$ref": "#/definitions/Coordinates"
|
||||
},
|
||||
"relpos": {
|
||||
"$ref": "#/definitions/Coordinates"
|
||||
},
|
||||
"size": {
|
||||
"$ref": "#/definitions/Coordinates"
|
||||
},
|
||||
"angle": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": ["pos", "relpos", "size", "angle"],
|
||||
"title": "Bbox"
|
||||
},
|
||||
"Pad": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"layers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Layer"
|
||||
},
|
||||
"minItems": 1,
|
||||
"maxItems": 2
|
||||
},
|
||||
"pos": {
|
||||
"$ref": "#/definitions/Coordinates"
|
||||
},
|
||||
"size": {
|
||||
"$ref": "#/definitions/Coordinates"
|
||||
},
|
||||
"angle": {
|
||||
"type": "number"
|
||||
},
|
||||
"shape": {
|
||||
"$ref": "#/definitions/Shape"
|
||||
},
|
||||
"svgpath": { "type": "string" },
|
||||
"polygons": { "$ref": "#/definitions/Polygons" },
|
||||
"radius": { "type": "number" },
|
||||
"chamfpos": { "type": "integer" },
|
||||
"chamfratio": { "type": "number" },
|
||||
"type": {
|
||||
"$ref": "#/definitions/PadType"
|
||||
},
|
||||
"pin1": {
|
||||
"type": "integer", "const": 1
|
||||
},
|
||||
"drillshape": {
|
||||
"$ref": "#/definitions/Drillshape"
|
||||
},
|
||||
"drillsize": {
|
||||
"$ref": "#/definitions/Coordinates"
|
||||
},
|
||||
"offset": {
|
||||
"$ref": "#/definitions/Coordinates"
|
||||
},
|
||||
"net": { "type": "string" }
|
||||
},
|
||||
"required": [
|
||||
"layers",
|
||||
"pos",
|
||||
"size",
|
||||
"shape",
|
||||
"type"
|
||||
],
|
||||
"allOf": [
|
||||
{
|
||||
"if": {
|
||||
"properties": { "shape": { "const": "custom" } }
|
||||
},
|
||||
"then": {
|
||||
"anyOf": [
|
||||
{ "required": [ "svgpath" ] },
|
||||
{ "required": [ "pos", "angle", "polygons" ] }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"properties": { "shape": { "const": "roundrect" } }
|
||||
},
|
||||
"then": {
|
||||
"required": [ "radius" ]
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"properties": { "shape": { "const": "chamfrect" } }
|
||||
},
|
||||
"then": {
|
||||
"required": [ "radius", "chamfpos", "chamfratio" ]
|
||||
}
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"properties": { "type": { "const": "th" } }
|
||||
},
|
||||
"then": {
|
||||
"required": [ "drillshape", "drillsize" ]
|
||||
}
|
||||
}
|
||||
],
|
||||
"title": "Pad"
|
||||
},
|
||||
"Metadata": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"revision": {
|
||||
"type": "string"
|
||||
},
|
||||
"company": {
|
||||
"type": "string"
|
||||
},
|
||||
"date": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["title", "revision", "company", "date"],
|
||||
"title": "Metadata"
|
||||
},
|
||||
"LayerDrawings": {
|
||||
"type": "object",
|
||||
"items": {
|
||||
"silkscreen": {
|
||||
"$ref": "#/definitions/DrawingSet"
|
||||
},
|
||||
"fabrication": {
|
||||
"$ref": "#/definitions/DrawingSet"
|
||||
}
|
||||
}
|
||||
},
|
||||
"DrawingArray": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Drawing"
|
||||
}
|
||||
},
|
||||
"Drawing": {
|
||||
"type": "object",
|
||||
"oneOf": [
|
||||
{ "$ref": "#/definitions/DrawingSegment" },
|
||||
{ "$ref": "#/definitions/DrawingRect" },
|
||||
{ "$ref": "#/definitions/DrawingCircle" },
|
||||
{ "$ref": "#/definitions/DrawingArc" },
|
||||
{ "$ref": "#/definitions/DrawingCurve" },
|
||||
{ "$ref": "#/definitions/DrawingPolygon" },
|
||||
{ "$ref": "#/definitions/DrawingText" }
|
||||
]
|
||||
},
|
||||
"DrawingSegment": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"type": { "type": "string", "const": "segment" },
|
||||
"start": { "$ref": "#/definitions/Coordinates" },
|
||||
"end": { "$ref": "#/definitions/Coordinates" },
|
||||
"width": { "type": "number" }
|
||||
},
|
||||
"required": ["type", "start", "end", "width"],
|
||||
"title": "DrawingSegment"
|
||||
},
|
||||
"DrawingRect": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"type": { "const": "rect" },
|
||||
"start": { "$ref": "#/definitions/Coordinates" },
|
||||
"end": { "$ref": "#/definitions/Coordinates" },
|
||||
"width": { "type": "number" }
|
||||
},
|
||||
"required": ["type", "start", "end", "width"],
|
||||
"title": "DrawingRect"
|
||||
},
|
||||
"DrawingCircle": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"type": { "const": "circle" },
|
||||
"start": { "$ref": "#/definitions/Coordinates" },
|
||||
"radius": { "type": "number" },
|
||||
"filled": { "type": "integer" },
|
||||
"width": { "type": "number" }
|
||||
},
|
||||
"required": ["type", "start", "radius", "width"],
|
||||
"title": "DrawingCircle"
|
||||
},
|
||||
"DrawingArc": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"type": { "const": "arc" },
|
||||
"width": { "type": "number" },
|
||||
"svgpath": { "type": "string" },
|
||||
"start": { "$ref": "#/definitions/Coordinates" },
|
||||
"radius": { "type": "number" },
|
||||
"startangle": { "type": "number" },
|
||||
"endangle": { "type": "number" }
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"width"
|
||||
],
|
||||
"anyOf": [
|
||||
{ "required": ["svgpath"] },
|
||||
{ "required": ["start", "radius", "startangle", "endangle"] }
|
||||
],
|
||||
"title": "DrawingArc"
|
||||
},
|
||||
"DrawingCurve": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"type": { "const": "curve" },
|
||||
"start": { "$ref": "#/definitions/Coordinates" },
|
||||
"end": { "$ref": "#/definitions/Coordinates" },
|
||||
"cpa": { "$ref": "#/definitions/Coordinates" },
|
||||
"cpb": { "$ref": "#/definitions/Coordinates" },
|
||||
"width": { "type": "number" }
|
||||
},
|
||||
"required": ["type", "start", "end", "cpa", "cpb", "width"],
|
||||
"title": "DrawingCurve"
|
||||
},
|
||||
"DrawingPolygon": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"type": { "const": "polygon" },
|
||||
"filled": { "type": "integer" },
|
||||
"width": { "type": "number" },
|
||||
"svgpath": { "type": "string" },
|
||||
"pos": { "$ref": "#/definitions/Coordinates" },
|
||||
"angle": { "type": "number" },
|
||||
"polygons": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/definitions/Coordinates" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["type"],
|
||||
"anyOf": [
|
||||
{ "required": ["svgpath"] },
|
||||
{ "required": ["pos", "angle", "polygons"] }
|
||||
],
|
||||
"title": "DrawingPolygon"
|
||||
},
|
||||
"DrawingText": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"svgpath": { "type": "string" },
|
||||
"thickness": { "type": "number" },
|
||||
"ref": { "type": "integer" , "const": 1 },
|
||||
"val": { "type": "integer" , "const": 1 }
|
||||
},
|
||||
"required": [
|
||||
"svgpath",
|
||||
"thickness"
|
||||
],
|
||||
"title": "DrawingText"
|
||||
},
|
||||
"Coordinates": {
|
||||
"type": "array",
|
||||
"items": { "type": "number" },
|
||||
"minItems": 2,
|
||||
"maxItems": 2
|
||||
},
|
||||
"Drillshape": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"circle",
|
||||
"oblong"
|
||||
],
|
||||
"title": "Drillshape"
|
||||
},
|
||||
"Layer": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"B",
|
||||
"F"
|
||||
],
|
||||
"title": "Layer"
|
||||
},
|
||||
"Shape": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"rect",
|
||||
"circle",
|
||||
"oval",
|
||||
"roundrect",
|
||||
"chamfrect",
|
||||
"custom"
|
||||
],
|
||||
"title": "Shape"
|
||||
},
|
||||
"PadType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"smd",
|
||||
"th"
|
||||
],
|
||||
"title": "PadType"
|
||||
},
|
||||
"Tracks": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"F": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/definitions/Track" }
|
||||
},
|
||||
"B": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/definitions/Track" }
|
||||
}
|
||||
},
|
||||
"required": [ "F", "B" ],
|
||||
"title": "Tracks"
|
||||
},
|
||||
"Track": {
|
||||
"type": "object",
|
||||
"oneOf":[
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"start": { "$ref": "#/definitions/Coordinates" },
|
||||
"end": { "$ref": "#/definitions/Coordinates" },
|
||||
"width": { "type": "number" },
|
||||
"drillsize": { "type": "number" },
|
||||
"net": { "type": "string" }
|
||||
},
|
||||
"required": [
|
||||
"start",
|
||||
"end",
|
||||
"width"
|
||||
]
|
||||
},
|
||||
{
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"center": { "$ref": "#/definitions/Coordinates" },
|
||||
"startangle": { "type": "number" },
|
||||
"endangle": { "type": "number" },
|
||||
"radius": { "type": "number" },
|
||||
"width": { "type": "number" },
|
||||
"net": { "type": "string" }
|
||||
},
|
||||
"required": [
|
||||
"center",
|
||||
"startangle",
|
||||
"endangle",
|
||||
"radius",
|
||||
"width"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"Zones": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"F": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/definitions/Zone" }
|
||||
},
|
||||
"B": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/definitions/Zone" }
|
||||
}
|
||||
},
|
||||
"required": [ "F", "B" ],
|
||||
"title": "Zones"
|
||||
},
|
||||
"Zone": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"svgpath": { "type": "string" },
|
||||
"polygons": {
|
||||
"$ref": "#/definitions/Polygons"
|
||||
},
|
||||
"net": { "type": "string" },
|
||||
"fillrule": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"nonzero",
|
||||
"evenodd"
|
||||
]
|
||||
}
|
||||
},
|
||||
"anyOf": [
|
||||
{ "required": [ "svgpath" ] },
|
||||
{ "required": [ "polygons" ] }
|
||||
],
|
||||
"title": "Zone"
|
||||
},
|
||||
"Polygons": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Coordinates"
|
||||
}
|
||||
}
|
||||
},
|
||||
"PolyLineArray": {
|
||||
"$ref": "#/definitions/Polygons"
|
||||
},
|
||||
"ReferenceSet": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": [
|
||||
{ "type": "string" },
|
||||
{ "type": "integer" }
|
||||
],
|
||||
"additionalItems": false
|
||||
}
|
||||
},
|
||||
"ExtraData": {
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"properties": {
|
||||
},
|
||||
"title": "ExtraData"
|
||||
},
|
||||
"FontData": {
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
"^.$" : {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"w": { "type": "number" },
|
||||
"l": { "$ref": "#/definitions/PolyLineArray" }
|
||||
},
|
||||
"additionalProperties" : false,
|
||||
"required": [
|
||||
"w",
|
||||
"l"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
538
3rdparty/plugins/org_openscopeproject_InteractiveHtmlBom/ecad/svgpath.py
vendored
Normal file
538
3rdparty/plugins/org_openscopeproject_InteractiveHtmlBom/ecad/svgpath.py
vendored
Normal file
@ -0,0 +1,538 @@
|
||||
"""This submodule contains very stripped down bare bones version of
|
||||
svgpathtools module:
|
||||
https://github.com/mathandy/svgpathtools
|
||||
|
||||
All external dependencies are removed. This code can parse path strings with
|
||||
segments and arcs, calculate bounding box and that's about it. This is all
|
||||
that is needed in ibom parsers at the moment.
|
||||
"""
|
||||
|
||||
# External dependencies
|
||||
from __future__ import division, absolute_import, print_function
|
||||
|
||||
import re
|
||||
from cmath import exp
|
||||
from math import sqrt, cos, sin, acos, degrees, radians, pi
|
||||
|
||||
|
||||
def clip(a, a_min, a_max):
|
||||
return min(a_max, max(a_min, a))
|
||||
|
||||
|
||||
class Line(object):
|
||||
def __init__(self, start, end):
|
||||
self.start = start
|
||||
self.end = end
|
||||
|
||||
def __repr__(self):
|
||||
return 'Line(start=%s, end=%s)' % (self.start, self.end)
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, Line):
|
||||
return NotImplemented
|
||||
return self.start == other.start and self.end == other.end
|
||||
|
||||
def __ne__(self, other):
|
||||
if not isinstance(other, Line):
|
||||
return NotImplemented
|
||||
return not self == other
|
||||
|
||||
def __len__(self):
|
||||
return 2
|
||||
|
||||
def bbox(self):
|
||||
"""returns the bounding box for the segment in the form
|
||||
(xmin, xmax, ymin, ymax)."""
|
||||
xmin = min(self.start.real, self.end.real)
|
||||
xmax = max(self.start.real, self.end.real)
|
||||
ymin = min(self.start.imag, self.end.imag)
|
||||
ymax = max(self.start.imag, self.end.imag)
|
||||
return xmin, xmax, ymin, ymax
|
||||
|
||||
|
||||
class Arc(object):
|
||||
def __init__(self, start, radius, rotation, large_arc, sweep, end,
|
||||
autoscale_radius=True):
|
||||
"""
|
||||
This should be thought of as a part of an ellipse connecting two
|
||||
points on that ellipse, start and end.
|
||||
Parameters
|
||||
----------
|
||||
start : complex
|
||||
The start point of the curve. Note: `start` and `end` cannot be the
|
||||
same. To make a full ellipse or circle, use two `Arc` objects.
|
||||
radius : complex
|
||||
rx + 1j*ry, where rx and ry are the radii of the ellipse (also
|
||||
known as its semi-major and semi-minor axes, or vice-versa or if
|
||||
rx < ry).
|
||||
Note: If rx = 0 or ry = 0 then this arc is treated as a
|
||||
straight line segment joining the endpoints.
|
||||
Note: If rx or ry has a negative sign, the sign is dropped; the
|
||||
absolute value is used instead.
|
||||
Note: If no such ellipse exists, the radius will be scaled so
|
||||
that one does (unless autoscale_radius is set to False).
|
||||
rotation : float
|
||||
This is the CCW angle (in degrees) from the positive x-axis of the
|
||||
current coordinate system to the x-axis of the ellipse.
|
||||
large_arc : bool
|
||||
Given two points on an ellipse, there are two elliptical arcs
|
||||
connecting those points, the first going the short way around the
|
||||
ellipse, and the second going the long way around the ellipse. If
|
||||
`large_arc == False`, the shorter elliptical arc will be used. If
|
||||
`large_arc == True`, then longer elliptical will be used.
|
||||
In other words, `large_arc` should be 0 for arcs spanning less than
|
||||
or equal to 180 degrees and 1 for arcs spanning greater than 180
|
||||
degrees.
|
||||
sweep : bool
|
||||
For any acceptable parameters `start`, `end`, `rotation`, and
|
||||
`radius`, there are two ellipses with the given major and minor
|
||||
axes (radii) which connect `start` and `end`. One which connects
|
||||
them in a CCW fashion and one which connected them in a CW
|
||||
fashion. If `sweep == True`, the CCW ellipse will be used. If
|
||||
`sweep == False`, the CW ellipse will be used. See note on curve
|
||||
orientation below.
|
||||
end : complex
|
||||
The end point of the curve. Note: `start` and `end` cannot be the
|
||||
same. To make a full ellipse or circle, use two `Arc` objects.
|
||||
autoscale_radius : bool
|
||||
If `autoscale_radius == True`, then will also scale `self.radius`
|
||||
in the case that no ellipse exists with the input parameters
|
||||
(see inline comments for further explanation).
|
||||
|
||||
Derived Parameters/Attributes
|
||||
-----------------------------
|
||||
self.theta : float
|
||||
This is the phase (in degrees) of self.u1transform(self.start).
|
||||
It is $\\theta_1$ in the official documentation and ranges from
|
||||
-180 to 180.
|
||||
self.delta : float
|
||||
This is the angular distance (in degrees) between the start and
|
||||
end of the arc after the arc has been sent to the unit circle
|
||||
through self.u1transform().
|
||||
It is $\\Delta\\theta$ in the official documentation and ranges
|
||||
from -360 to 360; being positive when the arc travels CCW and
|
||||
negative otherwise (i.e. is positive/negative when
|
||||
sweep == True/False).
|
||||
self.center : complex
|
||||
This is the center of the arc's ellipse.
|
||||
self.phi : float
|
||||
The arc's rotation in radians, i.e. `radians(self.rotation)`.
|
||||
self.rot_matrix : complex
|
||||
Equal to `exp(1j * self.phi)` which is also equal to
|
||||
`cos(self.phi) + 1j*sin(self.phi)`.
|
||||
|
||||
|
||||
Note on curve orientation (CW vs CCW)
|
||||
-------------------------------------
|
||||
The notions of clockwise (CW) and counter-clockwise (CCW) are reversed
|
||||
in some sense when viewing SVGs (as the y coordinate starts at the top
|
||||
of the image and increases towards the bottom).
|
||||
"""
|
||||
assert start != end
|
||||
assert radius.real != 0 and radius.imag != 0
|
||||
|
||||
self.start = start
|
||||
self.radius = abs(radius.real) + 1j * abs(radius.imag)
|
||||
self.rotation = rotation
|
||||
self.large_arc = bool(large_arc)
|
||||
self.sweep = bool(sweep)
|
||||
self.end = end
|
||||
self.autoscale_radius = autoscale_radius
|
||||
|
||||
# Convenience parameters
|
||||
self.phi = radians(self.rotation)
|
||||
self.rot_matrix = exp(1j * self.phi)
|
||||
|
||||
# Derive derived parameters
|
||||
self._parameterize()
|
||||
|
||||
def __repr__(self):
|
||||
params = (self.start, self.radius, self.rotation,
|
||||
self.large_arc, self.sweep, self.end)
|
||||
return ("Arc(start={}, radius={}, rotation={}, "
|
||||
"large_arc={}, sweep={}, end={})".format(*params))
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, Arc):
|
||||
return NotImplemented
|
||||
return self.start == other.start and self.end == other.end \
|
||||
and self.radius == other.radius \
|
||||
and self.rotation == other.rotation \
|
||||
and self.large_arc == other.large_arc and self.sweep == other.sweep
|
||||
|
||||
def __ne__(self, other):
|
||||
if not isinstance(other, Arc):
|
||||
return NotImplemented
|
||||
return not self == other
|
||||
|
||||
def _parameterize(self):
|
||||
# See http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes
|
||||
# my notation roughly follows theirs
|
||||
rx = self.radius.real
|
||||
ry = self.radius.imag
|
||||
rx_sqd = rx * rx
|
||||
ry_sqd = ry * ry
|
||||
|
||||
# Transform z-> z' = x' + 1j*y'
|
||||
# = self.rot_matrix**(-1)*(z - (end+start)/2)
|
||||
# coordinates. This translates the ellipse so that the midpoint
|
||||
# between self.end and self.start lies on the origin and rotates
|
||||
# the ellipse so that the its axes align with the xy-coordinate axes.
|
||||
# Note: This sends self.end to -self.start
|
||||
zp1 = (1 / self.rot_matrix) * (self.start - self.end) / 2
|
||||
x1p, y1p = zp1.real, zp1.imag
|
||||
x1p_sqd = x1p * x1p
|
||||
y1p_sqd = y1p * y1p
|
||||
|
||||
# Correct out of range radii
|
||||
# Note: an ellipse going through start and end with radius and phi
|
||||
# exists if and only if radius_check is true
|
||||
radius_check = (x1p_sqd / rx_sqd) + (y1p_sqd / ry_sqd)
|
||||
if radius_check > 1:
|
||||
if self.autoscale_radius:
|
||||
rx *= sqrt(radius_check)
|
||||
ry *= sqrt(radius_check)
|
||||
self.radius = rx + 1j * ry
|
||||
rx_sqd = rx * rx
|
||||
ry_sqd = ry * ry
|
||||
else:
|
||||
raise ValueError("No such elliptic arc exists.")
|
||||
|
||||
# Compute c'=(c_x', c_y'), the center of the ellipse in (x', y') coords
|
||||
# Noting that, in our new coord system, (x_2', y_2') = (-x_1', -x_2')
|
||||
# and our ellipse is cut out by of the plane by the algebraic equation
|
||||
# (x'-c_x')**2 / r_x**2 + (y'-c_y')**2 / r_y**2 = 1,
|
||||
# we can find c' by solving the system of two quadratics given by
|
||||
# plugging our transformed endpoints (x_1', y_1') and (x_2', y_2')
|
||||
tmp = rx_sqd * y1p_sqd + ry_sqd * x1p_sqd
|
||||
radicand = (rx_sqd * ry_sqd - tmp) / tmp
|
||||
try:
|
||||
radical = sqrt(radicand)
|
||||
except ValueError:
|
||||
radical = 0
|
||||
if self.large_arc == self.sweep:
|
||||
cp = -radical * (rx * y1p / ry - 1j * ry * x1p / rx)
|
||||
else:
|
||||
cp = radical * (rx * y1p / ry - 1j * ry * x1p / rx)
|
||||
|
||||
# The center in (x,y) coordinates is easy to find knowing c'
|
||||
self.center = exp(1j * self.phi) * cp + (self.start + self.end) / 2
|
||||
|
||||
# Now we do a second transformation, from (x', y') to (u_x, u_y)
|
||||
# coordinates, which is a translation moving the center of the
|
||||
# ellipse to the origin and a dilation stretching the ellipse to be
|
||||
# the unit circle
|
||||
u1 = (x1p - cp.real) / rx + 1j * (y1p - cp.imag) / ry
|
||||
u2 = (-x1p - cp.real) / rx + 1j * (-y1p - cp.imag) / ry
|
||||
|
||||
# clip in case of floating point error
|
||||
u1 = clip(u1.real, -1, 1) + 1j * clip(u1.imag, -1, 1)
|
||||
u2 = clip(u2.real, -1, 1) + 1j * clip(u2.imag, -1, 1)
|
||||
|
||||
# Now compute theta and delta (we'll define them as we go)
|
||||
# delta is the angular distance of the arc (w.r.t the circle)
|
||||
# theta is the angle between the positive x'-axis and the start point
|
||||
# on the circle
|
||||
if u1.imag > 0:
|
||||
self.theta = degrees(acos(u1.real))
|
||||
elif u1.imag < 0:
|
||||
self.theta = -degrees(acos(u1.real))
|
||||
else:
|
||||
if u1.real > 0: # start is on pos u_x axis
|
||||
self.theta = 0
|
||||
else: # start is on neg u_x axis
|
||||
# Note: This behavior disagrees with behavior documented in
|
||||
# http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes
|
||||
# where theta is set to 0 in this case.
|
||||
self.theta = 180
|
||||
|
||||
det_uv = u1.real * u2.imag - u1.imag * u2.real
|
||||
|
||||
acosand = u1.real * u2.real + u1.imag * u2.imag
|
||||
acosand = clip(acosand.real, -1, 1) + clip(acosand.imag, -1, 1)
|
||||
|
||||
if det_uv > 0:
|
||||
self.delta = degrees(acos(acosand))
|
||||
elif det_uv < 0:
|
||||
self.delta = -degrees(acos(acosand))
|
||||
else:
|
||||
if u1.real * u2.real + u1.imag * u2.imag > 0:
|
||||
# u1 == u2
|
||||
self.delta = 0
|
||||
else:
|
||||
# u1 == -u2
|
||||
# Note: This behavior disagrees with behavior documented in
|
||||
# http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes
|
||||
# where delta is set to 0 in this case.
|
||||
self.delta = 180
|
||||
|
||||
if not self.sweep and self.delta >= 0:
|
||||
self.delta -= 360
|
||||
elif self.large_arc and self.delta <= 0:
|
||||
self.delta += 360
|
||||
|
||||
def point(self, t):
|
||||
if t == 0:
|
||||
return self.start
|
||||
if t == 1:
|
||||
return self.end
|
||||
angle = radians(self.theta + t * self.delta)
|
||||
cosphi = self.rot_matrix.real
|
||||
sinphi = self.rot_matrix.imag
|
||||
rx = self.radius.real
|
||||
ry = self.radius.imag
|
||||
|
||||
# z = self.rot_matrix*(rx*cos(angle) + 1j*ry*sin(angle)) + self.center
|
||||
x = rx * cosphi * cos(angle) - ry * sinphi * sin(
|
||||
angle) + self.center.real
|
||||
y = rx * sinphi * cos(angle) + ry * cosphi * sin(
|
||||
angle) + self.center.imag
|
||||
return complex(x, y)
|
||||
|
||||
def bbox(self):
|
||||
"""returns a bounding box for the segment in the form
|
||||
(xmin, xmax, ymin, ymax)."""
|
||||
# a(t) = radians(self.theta + self.delta*t)
|
||||
# = (2*pi/360)*(self.theta + self.delta*t)
|
||||
# x'=0: ~~~~~~~~~
|
||||
# -rx*cos(phi)*sin(a(t)) = ry*sin(phi)*cos(a(t))
|
||||
# -(rx/ry)*cot(phi)*tan(a(t)) = 1
|
||||
# a(t) = arctan(-(ry/rx)tan(phi)) + pi*k === atan_x
|
||||
# y'=0: ~~~~~~~~~~
|
||||
# rx*sin(phi)*sin(a(t)) = ry*cos(phi)*cos(a(t))
|
||||
# (rx/ry)*tan(phi)*tan(a(t)) = 1
|
||||
# a(t) = arctan((ry/rx)*cot(phi))
|
||||
# atanres = arctan((ry/rx)*cot(phi)) === atan_y
|
||||
# ~~~~~~~~
|
||||
# (2*pi/360)*(self.theta + self.delta*t) = atanres + pi*k
|
||||
# Therefore, for both x' and y', we have...
|
||||
# t = ((atan_{x/y} + pi*k)*(360/(2*pi)) - self.theta)/self.delta
|
||||
# for all k s.t. 0 < t < 1
|
||||
from math import atan, tan
|
||||
|
||||
if cos(self.phi) == 0:
|
||||
atan_x = pi / 2
|
||||
atan_y = 0
|
||||
elif sin(self.phi) == 0:
|
||||
atan_x = 0
|
||||
atan_y = pi / 2
|
||||
else:
|
||||
rx, ry = self.radius.real, self.radius.imag
|
||||
atan_x = atan(-(ry / rx) * tan(self.phi))
|
||||
atan_y = atan((ry / rx) / tan(self.phi))
|
||||
|
||||
def angle_inv(ang, q): # inverse of angle from Arc.derivative()
|
||||
return ((ang + pi * q) * (360 / (2 * pi)) -
|
||||
self.theta) / self.delta
|
||||
|
||||
xtrema = [self.start.real, self.end.real]
|
||||
ytrema = [self.start.imag, self.end.imag]
|
||||
|
||||
for k in range(-4, 5):
|
||||
tx = angle_inv(atan_x, k)
|
||||
ty = angle_inv(atan_y, k)
|
||||
if 0 <= tx <= 1:
|
||||
xtrema.append(self.point(tx).real)
|
||||
if 0 <= ty <= 1:
|
||||
ytrema.append(self.point(ty).imag)
|
||||
return min(xtrema), max(xtrema), min(ytrema), max(ytrema)
|
||||
|
||||
|
||||
COMMANDS = set('MmZzLlHhVvCcSsQqTtAa')
|
||||
UPPERCASE = set('MZLHVCSQTA')
|
||||
|
||||
COMMAND_RE = re.compile("([MmZzLlHhVvCcSsQqTtAa])")
|
||||
FLOAT_RE = re.compile(r"[-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?")
|
||||
|
||||
|
||||
def _tokenize_path(path_def):
|
||||
for x in COMMAND_RE.split(path_def):
|
||||
if x in COMMANDS:
|
||||
yield x
|
||||
for token in FLOAT_RE.findall(x):
|
||||
yield token
|
||||
|
||||
|
||||
def parse_path(pathdef, logger, current_pos=0j):
|
||||
# In the SVG specs, initial movetos are absolute, even if
|
||||
# specified as 'm'. This is the default behavior here as well.
|
||||
# But if you pass in a current_pos variable, the initial moveto
|
||||
# will be relative to that current_pos. This is useful.
|
||||
elements = list(_tokenize_path(pathdef))
|
||||
# Reverse for easy use of .pop()
|
||||
elements.reverse()
|
||||
absolute = False
|
||||
|
||||
segments = []
|
||||
|
||||
start_pos = None
|
||||
command = None
|
||||
|
||||
while elements:
|
||||
|
||||
if elements[-1] in COMMANDS:
|
||||
# New command.
|
||||
command = elements.pop()
|
||||
absolute = command in UPPERCASE
|
||||
command = command.upper()
|
||||
else:
|
||||
# If this element starts with numbers, it is an implicit command
|
||||
# and we don't change the command. Check that it's allowed:
|
||||
if command is None:
|
||||
raise ValueError(
|
||||
"Unallowed implicit command in %s, position %s" % (
|
||||
pathdef, len(pathdef.split()) - len(elements)))
|
||||
|
||||
if command == 'M':
|
||||
# Moveto command.
|
||||
x = elements.pop()
|
||||
y = elements.pop()
|
||||
pos = float(x) + float(y) * 1j
|
||||
if absolute:
|
||||
current_pos = pos
|
||||
else:
|
||||
current_pos += pos
|
||||
|
||||
# when M is called, reset start_pos
|
||||
# This behavior of Z is defined in svg spec:
|
||||
# http://www.w3.org/TR/SVG/paths.html#PathDataClosePathCommand
|
||||
start_pos = current_pos
|
||||
|
||||
# Implicit moveto commands are treated as lineto commands.
|
||||
# So we set command to lineto here, in case there are
|
||||
# further implicit commands after this moveto.
|
||||
command = 'L'
|
||||
|
||||
elif command == 'Z':
|
||||
# Close path
|
||||
if not (current_pos == start_pos):
|
||||
segments.append(Line(current_pos, start_pos))
|
||||
current_pos = start_pos
|
||||
command = None
|
||||
|
||||
elif command == 'L':
|
||||
x = elements.pop()
|
||||
y = elements.pop()
|
||||
pos = float(x) + float(y) * 1j
|
||||
if not absolute:
|
||||
pos += current_pos
|
||||
segments.append(Line(current_pos, pos))
|
||||
current_pos = pos
|
||||
|
||||
elif command == 'H':
|
||||
x = elements.pop()
|
||||
pos = float(x) + current_pos.imag * 1j
|
||||
if not absolute:
|
||||
pos += current_pos.real
|
||||
segments.append(Line(current_pos, pos))
|
||||
current_pos = pos
|
||||
|
||||
elif command == 'V':
|
||||
y = elements.pop()
|
||||
pos = current_pos.real + float(y) * 1j
|
||||
if not absolute:
|
||||
pos += current_pos.imag * 1j
|
||||
segments.append(Line(current_pos, pos))
|
||||
current_pos = pos
|
||||
|
||||
elif command == 'C':
|
||||
logger.warn('Encountered Cubic Bezier segment. '
|
||||
'It is currently not supported and will be replaced '
|
||||
'by a line segment.')
|
||||
for i in range(4):
|
||||
# ignore control points
|
||||
elements.pop()
|
||||
end = float(elements.pop()) + float(elements.pop()) * 1j
|
||||
|
||||
if not absolute:
|
||||
end += current_pos
|
||||
|
||||
segments.append(Line(current_pos, end))
|
||||
current_pos = end
|
||||
|
||||
elif command == 'S':
|
||||
logger.warn('Encountered Quadratic Bezier segment. '
|
||||
'It is currently not supported and will be replaced '
|
||||
'by a line segment.')
|
||||
for i in range(2):
|
||||
# ignore control points
|
||||
elements.pop()
|
||||
end = float(elements.pop()) + float(elements.pop()) * 1j
|
||||
|
||||
if not absolute:
|
||||
end += current_pos
|
||||
|
||||
segments.append(Line(current_pos, end))
|
||||
current_pos = end
|
||||
|
||||
elif command == 'Q':
|
||||
logger.warn('Encountered Quadratic Bezier segment. '
|
||||
'It is currently not supported and will be replaced '
|
||||
'by a line segment.')
|
||||
for i in range(2):
|
||||
# ignore control points
|
||||
elements.pop()
|
||||
end = float(elements.pop()) + float(elements.pop()) * 1j
|
||||
|
||||
if not absolute:
|
||||
end += current_pos
|
||||
|
||||
segments.append(Line(current_pos, end))
|
||||
current_pos = end
|
||||
|
||||
elif command == 'T':
|
||||
logger.warn('Encountered Quadratic Bezier segment. '
|
||||
'It is currently not supported and will be replaced '
|
||||
'by a line segment.')
|
||||
|
||||
end = float(elements.pop()) + float(elements.pop()) * 1j
|
||||
|
||||
if not absolute:
|
||||
end += current_pos
|
||||
|
||||
segments.append(Line(current_pos, end))
|
||||
current_pos = end
|
||||
|
||||
elif command == 'A':
|
||||
radius = float(elements.pop()) + float(elements.pop()) * 1j
|
||||
rotation = float(elements.pop())
|
||||
arc = float(elements.pop())
|
||||
sweep = float(elements.pop())
|
||||
end = float(elements.pop()) + float(elements.pop()) * 1j
|
||||
|
||||
if not absolute:
|
||||
end += current_pos
|
||||
|
||||
segments.append(
|
||||
Arc(current_pos, radius, rotation, arc, sweep, end))
|
||||
current_pos = end
|
||||
|
||||
return segments
|
||||
|
||||
|
||||
def create_path(lines, circles=[]):
|
||||
"""Returns a path d-string."""
|
||||
|
||||
def limit_digits(val):
|
||||
return format(val, '.6f').rstrip('0').replace(',', '.').rstrip('.')
|
||||
|
||||
def different_points(a, b):
|
||||
return abs(a[0] - b[0]) > 1e-6 or abs(a[1] - b[1]) > 1e-6
|
||||
|
||||
parts = []
|
||||
|
||||
for i, line in enumerate(lines):
|
||||
if i == 0 or different_points(lines[i - 1][-1], line[0]):
|
||||
parts.append('M{},{}'.format(*map(limit_digits, line[0])))
|
||||
for point in line[1:]:
|
||||
parts.append('L{},{}'.format(*map(limit_digits, point)))
|
||||
|
||||
for circle in circles:
|
||||
cx, cy, r = circle[0][0], circle[0][1], circle[1]
|
||||
parts.append('M{},{}'.format(limit_digits(cx - r), limit_digits(cy)))
|
||||
parts.append('a {},{} 0 1,0 {},0'.format(
|
||||
*map(limit_digits, [r, r, r + r])))
|
||||
parts.append('a {},{} 0 1,0 -{},0'.format(
|
||||
*map(limit_digits, [r, r, r + r])))
|
||||
|
||||
return ''.join(parts)
|
Reference in New Issue
Block a user