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

494 lines
16 KiB
Python

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