From bd00731355f165acd9d8f9561a03fc348e44a00b Mon Sep 17 00:00:00 2001 From: "Salvador E. Tropea" Date: Fri, 27 Jan 2023 23:57:44 -0300 Subject: [PATCH] [Experiments][Added] EasyEDA 3D model downloader prototype Related to #380 --- experiments/EasyEDA/README.md | 10 + experiments/EasyEDA/easyeda_api.py | 283 +++++++++++++++++++++++++++++ 2 files changed, 293 insertions(+) create mode 100644 experiments/EasyEDA/README.md create mode 100644 experiments/EasyEDA/easyeda_api.py diff --git a/experiments/EasyEDA/README.md b/experiments/EasyEDA/README.md new file mode 100644 index 00000000..08bfd288 --- /dev/null +++ b/experiments/EasyEDA/README.md @@ -0,0 +1,10 @@ +# LCSC 3D Models + +This is part of the easyeda2kicad code. +The whole code depends on pydantic and typing_extensions. +The dependencies didn't even install using pip on Debian 11.6. + +This is the portion of the code needed to download the 3D model for an LCSC component code. +The generated WRL has colors, but not correct materials. + +This is all related to the issue #380 diff --git a/experiments/EasyEDA/easyeda_api.py b/experiments/EasyEDA/easyeda_api.py new file mode 100644 index 00000000..1f377da1 --- /dev/null +++ b/experiments/EasyEDA/easyeda_api.py @@ -0,0 +1,283 @@ +# 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)}")