KiBot/experiments/EasyEDA/easyeda_api.py

306 lines
9.4 KiB
Python

# Author: uPesy
# License: AGPL v3
# Project: https://github.com/uPesy/easyeda2kicad.py
# Global imports
from __future__ import annotations
from dataclasses import dataclass
import logging
import os
import requests
import re
import textwrap
#
import pprint
import json
import pickle
API_ENDPOINT = "https://easyeda.com/api/products/{lcsc_id}/components?version=6.4.19.5"
ENDPOINT_3D_MODEL = "https://easyeda.com/analyzer/api/3dmodel/{uuid}"
VRML_HEADER = "#VRML V2.0 utf8\n# 3D model generated by KiBot (using easyeda2kicad.py code)\n"
USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:102.0) Gecko/20100101 Firefox/102.0'
class EasyedaApi:
def __init__(self) -> None:
self.headers = {"Accept-Encoding": "gzip, deflate",
"Accept": "application/json, text/javascript, */*; q=0.01",
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
"User-Agent": USER_AGENT}
def get_info_from_easyeda_api(self, lcsc_id: str) -> dict:
r = requests.get(url=API_ENDPOINT.format(lcsc_id=lcsc_id), headers=self.headers)
api_response = r.json()
if not api_response or (
"code" in api_response and api_response["success"] is False
):
logging.debug(f"{api_response}")
return {}
return r.json()
def get_cad_data_of_component(self, lcsc_id: str) -> dict:
cp_cad_info = self.get_info_from_easyeda_api(lcsc_id=lcsc_id)
if cp_cad_info == {}:
return {}
return cp_cad_info["result"]
def get_raw_3d_model_obj(self, uuid: str) -> str:
r = requests.get(
url=ENDPOINT_3D_MODEL.format(uuid=uuid),
headers={"User-Agent": self.headers["User-Agent"]},
)
if r.status_code != requests.codes.ok:
logging.error(f"No 3D model data found for uuid:{uuid} on easyeda")
return None
return r.content.decode()
def convert_to_mm(dim: float) -> float:
return float(dim) * 10 * 0.0254
@dataclass
class Ee3dModelBase:
x: float = 0.0
y: float = 0.0
z: float = 0.0
def convert_to_mm(self) -> None:
self.x = convert_to_mm(self.x)
self.y = convert_to_mm(self.y)
self.z = convert_to_mm(self.z)
@dataclass
class Ee3dModel:
name: str
uuid: str
translation: Ee3dModelBase
rotation: Ee3dModelBase
raw_obj: str = None
def convert_to_mm(self) -> None:
self.translation.convert_to_mm()
# self.translation.z = self.translation.z
@dataclass
class Ki3dModelBase:
x: float = 0.0
y: float = 0.0
z: float = 0.0
@dataclass
class Ki3dModel:
name: str
translation: Ki3dModelBase
rotation: Ki3dModelBase
raw_wrl: str = None
class Easyeda3dModelImporter:
def __init__(self, easyeda_cp_cad_data, download_raw_3d_model: bool):
self.input = easyeda_cp_cad_data
self.download_raw_3d_model = download_raw_3d_model
self.output = self.create_3d_model()
def create_3d_model(self):
ee_data = (
self.input["packageDetail"]["dataStr"]["shape"]
if isinstance(self.input, dict)
else self.input
)
if model_3d_info := self.get_3d_model_info(ee_data=ee_data):
model_3d: Ee3dModel = self.parse_3d_model_info(info=model_3d_info)
if self.download_raw_3d_model:
model_3d.raw_obj = EasyedaApi().get_raw_3d_model_obj(uuid=model_3d.uuid)
return model_3d
logging.warning("No 3D model available for this component")
return None
def get_3d_model_info(self, ee_data: str) -> dict:
for line in ee_data:
ee_designator = line.split("~")[0]
if ee_designator == "SVGNODE":
raw_json = line.split("~")[1:][0]
return json.loads(raw_json)["attrs"]
return {}
def parse_3d_model_info(self, info: dict):
return Ee3dModel(
name=info["title"],
uuid=info["uuid"],
translation=Ee3dModelBase(
x=info["c_origin"].split(",")[0],
y=info["c_origin"].split(",")[1],
z=info["z"],
),
rotation=Ee3dModelBase(*(info["c_rotation"].split(","))),
)
def get_materials(obj_data: str) -> dict:
material_regex = "newmtl .*?endmtl"
matchs = re.findall(pattern=material_regex, string=obj_data, flags=re.DOTALL)
materials = {}
for match in matchs:
material = {}
for value in match.splitlines():
if value.startswith("newmtl"):
material_id = value.split(" ")[1]
elif value.startswith("Ka"):
material["ambient_color"] = value.split(" ")[1:]
elif value.startswith("Kd"):
material["diffuse_color"] = value.split(" ")[1:]
elif value.startswith("Ks"):
material["specular_color"] = value.split(" ")[1:]
elif value.startswith("d"):
material["transparency"] = value.split(" ")[1]
materials[material_id] = material
# logging.error(materials)
return materials
def get_vertices(obj_data: str) -> list:
vertices_regex = "v (.*?)\n"
matchs = re.findall(pattern=vertices_regex, string=obj_data, flags=re.DOTALL)
return [
" ".join([str(round(float(coord) / 2.54, 4)) for coord in vertice.split(" ")])
for vertice in matchs
]
def generate_wrl_model(model_3d: Ee3dModel) -> Ki3dModel:
materials = get_materials(obj_data=model_3d.raw_obj)
vertices = get_vertices(obj_data=model_3d.raw_obj)
raw_wrl = VRML_HEADER
# Define all the materials
for id, mat in materials.items():
mat_str = textwrap.dedent(
f"""
Shape {{
appearance Appearance {{
material DEF MATERIAL_{id} Material {{
ambientIntensity 0.2
diffuseColor {' '.join(mat['diffuse_color'])}
specularColor {' '.join(mat['specular_color'])}
shininess 0.5
transparency 0.0
}}
}}
}}"""
)
raw_wrl += mat_str
# Define the shapes
shapes = model_3d.raw_obj.split("usemtl")[1:]
for shape in shapes:
lines = shape.splitlines()
material_id = lines[0].replace(" ", "")
material = materials[material_id]
index_counter = 0
link_dict = {}
coord_index = []
points = []
for line in lines[1:]:
if len(line) > 0:
face = [int(index) for index in line.replace("//", "").split(" ")[1:]]
face_index = []
for index in face:
if index not in link_dict:
link_dict[index] = index_counter
face_index.append(str(index_counter))
points.append(vertices[index - 1])
index_counter += 1
else:
face_index.append(str(link_dict[index]))
face_index.append("-1")
coord_index.append(",".join(face_index) + ",")
points.insert(-1, points[-1])
shape_str = textwrap.dedent(
f"""
Shape {{
geometry IndexedFaceSet {{
ccw TRUE
solid FALSE
coord DEF co Coordinate {{
point [
{(", ").join(points)}
]
}}
coordIndex [
{"".join(coord_index)}
]
}}
appearance Appearance {{material USE MATERIAL_{material_id}}}
}}"""
)
raw_wrl += shape_str
return Ki3dModel(
translation=None, rotation=None, name=model_3d.name, raw_wrl=raw_wrl
)
class Exporter3dModelKicad:
def __init__(self, model_3d: Ee3dModel):
self.input = model_3d
self.output = (
generate_wrl_model(model_3d=model_3d)
if model_3d and model_3d.raw_obj
else None
)
def export(self, lib_path: str) -> None:
if self.output:
with open(
file=f"{lib_path}.3dshapes/{self.output.name}.wrl",
mode="w",
encoding="utf-8",
) as my_lib:
my_lib.write(self.output.raw_wrl)
component_id = 'C2895617'
#
if False:
a = EasyedaApi()
res = a.get_cad_data_of_component(component_id)
print(pprint.pformat(res))
if not res:
logging.error('Not found')
exit(1)
c = Easyeda3dModelImporter(res, True)
# print("********************************************")
# print(pprint.pformat(c.__dict__))
with open('model.pkl', 'wb') as file:
pickle.dump(c, file)
else:
with open('model.pkl', 'rb') as file:
c = pickle.load(file)
# print(pprint.pformat(c.__dict__))
with open('model.obj', 'w') as file:
file.write(c.output.raw_obj)
exporter = Exporter3dModelKicad(model_3d=c.output)
os.makedirs('a.3dshapes', exist_ok=True)
exporter.export('a')
if exporter.output:
filename = f"{exporter.output.name}.wrl"
lib_path = "a.3dshapes"
logging.error(f"Created 3D model for ID: {component_id}\n"
f" 3D model name: {exporter.output.name}\n"
f" 3D model path: {os.path.join(lib_path, filename)}")