# 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 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 # 3D model generated by easyeda2kicad.py (https://github.com/uPesy/easyeda2kicad.py) """ # ------------------------------------------------------------ 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": "easyeda2kicad v0.6.2", } 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 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 shapes = model_3d.raw_obj.split("usemtl")[1:] for shape in shapes: lines = shape.splitlines() material = materials[lines[0].replace(" ", "")] 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{{ appearance Appearance {{ material Material {{ diffuseColor {' '.join(material['diffuse_color'])} specularColor {' '.join(material['specular_color'])} ambientIntensity 0.2 transparency {material['transparency']} shininess 0.5 }} }} geometry IndexedFaceSet {{ ccw TRUE solid FALSE coord DEF co Coordinate {{ point [ {(", ").join(points)} ] }} coordIndex [ {"".join(coord_index)} ] }} }}""" ) raw_wrl += shape_str logging.error('Aca') 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: logging.error('Aca 1') 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 = 'C181094' a = EasyedaApi() res = a.get_cad_data_of_component(component_id) # print(pprint.pformat(res)) c = Easyeda3dModelImporter(res, True) # print(pprint.pformat(c.__dict__)) 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)}")