[Experiments][Added] EasyEDA 3D model downloader prototype

Related to #380
This commit is contained in:
Salvador E. Tropea 2023-01-27 23:57:44 -03:00
parent 676ee9ecd9
commit bd00731355
2 changed files with 293 additions and 0 deletions

View File

@ -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

View File

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