# 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)}")