my-kicad-user-folder/3rdparty/plugins/org_openscopeproject_InteractiveHtmlBom/ecad/common.py
2025-03-21 13:28:36 +08:00

252 lines
8.1 KiB
Python

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