Added mechanism to download 3D models from EasyEDA
- Using LCSC codes Closes #380
This commit is contained in:
parent
305bfb7d0c
commit
df085fa8bc
|
|
@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Internal templates import
|
||||
- Better support for wrong pre-flight options (#360)
|
||||
- A mechanism to cache downloaded 3D models
|
||||
- Support to download 3D models from EasyEDA (using LCSC codes)
|
||||
- Global options:
|
||||
- field_lcsc_part: to select the LCSC/JLCPCB part field
|
||||
- New outputs:
|
||||
|
|
|
|||
|
|
@ -5928,6 +5928,12 @@ I strongly suggest including all used 3D models in your repo.
|
|||
You can then use `${KIPRJMOD}` as base for the path to the models, this will be expanded to the current path to your project.
|
||||
So you can use things like `${KIPRJMOD}/3D/MODEL_NAME` and store all the 3D models in the *3D* folder inside your project folder.
|
||||
|
||||
### LCSC/JLCPCB/EasyEDA 3D models
|
||||
|
||||
KiBot can download 3D models for components that has an LCSC code and that has a 3D model at [EasyEDA](https://easyeda.com/).
|
||||
If the 3D model is used locally, but not found in the repo, KiBot will try to download it.
|
||||
Use the `field_lcsc_part` option if KiBot fails to detect the schematic field containing the LCSC code.
|
||||
|
||||
### 3D models aliases
|
||||
|
||||
This is a very limited feature in KiCad. You can define an `ALIAS` and then use `ALIAS:MODEL_NAME`.
|
||||
|
|
|
|||
|
|
@ -2059,6 +2059,12 @@ I strongly suggest including all used 3D models in your repo.
|
|||
You can then use `${KIPRJMOD}` as base for the path to the models, this will be expanded to the current path to your project.
|
||||
So you can use things like `${KIPRJMOD}/3D/MODEL_NAME` and store all the 3D models in the *3D* folder inside your project folder.
|
||||
|
||||
### LCSC/JLCPCB/EasyEDA 3D models
|
||||
|
||||
KiBot can download 3D models for components that has an LCSC code and that has a 3D model at [EasyEDA](https://easyeda.com/).
|
||||
If the 3D model is used locally, but not found in the repo, KiBot will try to download it.
|
||||
Use the `field_lcsc_part` option if KiBot fails to detect the schematic field containing the LCSC code.
|
||||
|
||||
### 3D models aliases
|
||||
|
||||
This is a very limited feature in KiCad. You can define an `ALIAS` and then use `ALIAS:MODEL_NAME`.
|
||||
|
|
|
|||
|
|
@ -8,3 +8,97 @@ This is the portion of the code needed to download the 3D model for an LCSC comp
|
|||
The generated WRL has colors, but not correct materials.
|
||||
|
||||
This is all related to the issue #380
|
||||
|
||||
# Colors
|
||||
|
||||
diffuse - specular
|
||||
|
||||
## Analyzed
|
||||
|
||||
### SOT-23-3P_L2.9-W1.3-H1.0-LS2.4-P0.95.wrl (C181094)
|
||||
- LCEDA: 1.0 1.0 1.0 - 0.8784313725490196 0.8784313725490196 0.8784313725490196
|
||||
- Silver pins:0.7686274509803922 0.7686274509803922 0.7686274509803922 - 0.615686274509804 0.615686274509804 0.615686274509804
|
||||
- Plastic body: 0.25098039215686274 0.25098039215686274 0.25098039215686274 - 0.07450980392156863 0.07450980392156863 0.07450980392156863
|
||||
- Text legend: 0.45098039215686275 0.43137254901960786 0.4196078431372549 - 0.03137254901960784 0.03137254901960784 0.03137254901960784
|
||||
|
||||
### CONN-TH_4P-P2.54_MOLEX_0705430004.wrl (C3020650)
|
||||
|
||||
This model is wrong, the pins should be gold
|
||||
|
||||
- Silver pins:(also legend) 0.8313725490196079 0.8313725490196079 0.8196078431372549 - 0.6666666666666666 0.6666666666666666 0.6549019607843137
|
||||
- Plastic body: 0.20784313725490197 0.20784313725490197 0.20784313725490197 - 0.01568627450980392 0.01568627450980392 0.01568627450980392
|
||||
- Plastic body pin 1 arrow: 0.07058823529411765 0.07058823529411765 0.07058823529411765 - 0.00392156862745098 0.00392156862745098 0.00392156862745098
|
||||
- Error color used to fill the legend ... should be plastic body, an error: 0.792156862745098 0.8196078431372549 0.9333333333333333 - 0.396078431372549 0.4117647058823529 0.4666666666666667
|
||||
|
||||
### HDR2.54-M-LI-1X1P.wrl (C213440)
|
||||
|
||||
- Golden pins: 0.9490196078431372 0.7607843137254902 0.1803921568627451 - 0.7607843137254902 0.6078431372549019 0.1450980392156863
|
||||
- Plastic body: 0.17647058823529413 0.17647058823529413 0.17647058823529413 - 0.050980392156862744 0.050980392156862744 0.050980392156862744
|
||||
- LCEDA: 0.0 0.0 0.0 - 0.0 0.0 0.0
|
||||
|
||||
### SMB_L4.6-W3.6-LS5.3-BI.wrl (C78395)
|
||||
|
||||
- Plastic body: 0.25098039215686274 0.25098039215686274 0.25098039215686274 - 0.12549019607843137 0.12549019607843137 0.12549019607843137
|
||||
- Silver pins: 0.7803921568627451 0.7803921568627451 0.7803921568627451 - 0.38823529411764707 0.38823529411764707 0.38823529411764707
|
||||
- LCEDA: 1.0 1.0 1.0 - 0.8784313725490196 0.8784313725490196 0.8784313725490196
|
||||
|
||||
### SMA_L4.3-W2.6-LS5.2-RD (C8678)
|
||||
|
||||
- Plastic body: 0.25098039215686274 0.25098039215686274 0.25098039215686274 - 0.12549019607843137 0.12549019607843137 0.12549019607843137
|
||||
- Silver pins: 0.7803921568627451 0.7803921568627451 0.7803921568627451 - 0.38823529411764707 0.38823529411764707 0.38823529411764707
|
||||
- Text legend: 0.6666666666666666 0.6666666666666666 0.6666666666666666 - 0.3333333333333333 0.3333333333333333 0.3333333333333333
|
||||
- LCEDA 1.0 1.0 1.0 - 0.8784313725490196 0.8784313725490196 0.8784313725490196
|
||||
- Pin 1 bars 0.8509803921568627 0.8509803921568627 0.8509803921568627 - 0.4235294117647059 0.4235294117647059 0.4235294117647059
|
||||
|
||||
### SOT-23-6_L2.9-W1.6-H1.5-LS2.8-P0.95.wrl (C202311)
|
||||
|
||||
- Plastic body: 0.25098039215686274 0.25098039215686274 0.25098039215686274 - 0.07450980392156863 0.07450980392156863 0.07450980392156863
|
||||
- Text legend: 0.45098039215686275 0.43137254901960786 0.4196078431372549 - 0.03137254901960784 0.03137254901960784 0.03137254901960784
|
||||
- LCEDA 1.0 1.0 1.0 - 0.8784313725490196 0.8784313725490196 0.8784313725490196
|
||||
- Silver pins: 0.7686274509803922 0.7686274509803922 0.7686274509803922 - 0.615686274509804 0.615686274509804 0.615686274509804
|
||||
|
||||
### SOIC-8_L4.9-W3.9-P1.27-LS6.0-BL-EP3.3-1.wrl (C2760005)
|
||||
|
||||
- Plastic body: 0.20784313725490197 0.20784313725490197 0.20784313725490197 - 0.01568627450980392 0.01568627450980392 0.01568627450980392
|
||||
- Pin 1 circle: 0.34901960784313724 0.34901960784313724 0.34901960784313724 - 0.023529411764705882 0.023529411764705882 0.023529411764705882
|
||||
- Silver pins: 0.6666666666666666 0.6666666666666666 0.6666666666666666 - 0.4666666666666667 0.4666666666666667 0.4666666666666667
|
||||
- LCEDA: 1.0 1.0 1.0 - 0.5019607843137255 0.5019607843137255 0.5019607843137255
|
||||
|
||||
### SW-SMD_4P-L5.2-W5.2-H1.5-LS6.4-P3.70 (C318884)
|
||||
|
||||
- Metal body 0.8313725490196079 0.8392156862745098 0.8313725490196079 - 0.4980392156862745 0.5019607843137255 0.4980392156862745
|
||||
- Plastic inner body 0.07058823529411765 0.07058823529411765 0.07058823529411765 - 0.00392156862745098 0.00392156862745098 0.00392156862745098
|
||||
- Golden button 0.9490196078431372 0.7607843137254902 0.1803921568627451 - 0.7607843137254902 0.6078431372549019 0.1450980392156863
|
||||
- Plastic body 0.2980392156862745 0.2980392156862745 0.2980392156862745 - 0.09019607843137255 0.09019607843137255 0.09019607843137255
|
||||
- Silver pins: 0.7686274509803922 0.7686274509803922 0.7686274509803922 - 0.615686274509804 0.615686274509804 0.615686274509804
|
||||
- LCEDA 1.0 1.0 1.0 - 0.8784313725490196 0.8784313725490196 0.8784313725490196
|
||||
|
||||
### SW-TH_SPEF110100 C115366
|
||||
|
||||
This model uses d = 1, but isn't transparent
|
||||
|
||||
- Metal body and Silver pins 0.9686274509803922 0.9686274509803922 0.9686274509803922 - 0.7529411764705882 0.7529411764705882 0.7529411764705882
|
||||
- Plastic body 0.25882352941176473 0.25882352941176473 0.25882352941176473 - 0.7529411764705882 0.7529411764705882 0.7529411764705882
|
||||
|
||||
## Groups
|
||||
|
||||
### Silver Metal
|
||||
|
||||
- 0.969 0.969 0.969 - 0.753 0.753 0.753
|
||||
- 0.831 0.831 0.820 - 0.667 0.667 0.655 (1,56% de error en B)
|
||||
- 0.780 0.780 0.780 - 0.388 0.388 0.388 (2x)
|
||||
- 0.769 0.769 0.769 - 0.616 0.616 0.616 (3x)
|
||||
- 0.667 0.667 0.667 - 0.467 0.467 0.467
|
||||
|
||||
### Plastic body
|
||||
|
||||
- 0.298 0.298 0.298 - 0.090 0.090 0.090
|
||||
- 0.259 0.259 0.259 - 0.753 0.753 0.753
|
||||
- 0.251 0.251 0.251 - 0.125 0.125 0.125 (2x)
|
||||
- 0.251 0.251 0.251 - 0.075 0.075 0.075 (2x)
|
||||
- 0.208 0.208 0.208 - 0.016 0.016 0.016 (2x)
|
||||
- 0.176 0.176 0.176 - 0.051 0.051 0.051
|
||||
|
||||
### Golden Metal
|
||||
|
||||
- 0.949 0.761 0.180 - 0.761 0.608 0.145 (2x)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
# License: AGPL v3
|
||||
# Project: https://github.com/uPesy/easyeda2kicad.py
|
||||
# Global imports
|
||||
from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
import os
|
||||
|
|
@ -18,46 +17,41 @@ 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'
|
||||
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:
|
||||
def __init__(self):
|
||||
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:
|
||||
def get_info_from_easyeda_api(self, lcsc_id: str):
|
||||
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
|
||||
):
|
||||
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:
|
||||
def get_cad_data_of_component(self, lcsc_id: str):
|
||||
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"]},
|
||||
)
|
||||
def get_raw_3d_model_obj(self, uuid: 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:
|
||||
def convert_to_mm(dim: float):
|
||||
return float(dim) * 10 * 0.0254
|
||||
|
||||
|
||||
|
|
@ -102,20 +96,17 @@ class Ki3dModel:
|
|||
|
||||
|
||||
class Easyeda3dModelImporter:
|
||||
def __init__(self, easyeda_cp_cad_data, download_raw_3d_model: bool):
|
||||
def __init__(self, easyeda_cp_cad_data, download_raw_3d_model=True):
|
||||
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
|
||||
)
|
||||
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)
|
||||
model_3d_info = self.get_3d_model_info(ee_data=ee_data)
|
||||
if model_3d_info:
|
||||
model_3d = 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
|
||||
|
|
@ -123,7 +114,7 @@ class Easyeda3dModelImporter:
|
|||
logging.warning("No 3D model available for this component")
|
||||
return None
|
||||
|
||||
def get_3d_model_info(self, ee_data: str) -> dict:
|
||||
def get_3d_model_info(self, ee_data: str):
|
||||
for line in ee_data:
|
||||
ee_designator = line.split("~")[0]
|
||||
if ee_designator == "SVGNODE":
|
||||
|
|
@ -144,7 +135,7 @@ class Easyeda3dModelImporter:
|
|||
)
|
||||
|
||||
|
||||
def get_materials(obj_data: str) -> dict:
|
||||
def get_materials(obj_data: str):
|
||||
|
||||
material_regex = "newmtl .*?endmtl"
|
||||
matchs = re.findall(pattern=material_regex, string=obj_data, flags=re.DOTALL)
|
||||
|
|
@ -165,18 +156,14 @@ def get_materials(obj_data: str) -> dict:
|
|||
material["transparency"] = value.split(" ")[1]
|
||||
|
||||
materials[material_id] = material
|
||||
# logging.error(materials)
|
||||
return materials
|
||||
|
||||
|
||||
def get_vertices(obj_data: str) -> list:
|
||||
def get_vertices(obj_data: str):
|
||||
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
|
||||
]
|
||||
return [" ".join([str(round(float(coord) / 2.54, 4)) for coord in vertice.split(" ")]) for vertice in matchs]
|
||||
|
||||
|
||||
def map_materials(mats):
|
||||
|
|
@ -226,7 +213,7 @@ def map_materials(mats):
|
|||
return mat_map
|
||||
|
||||
|
||||
def generate_wrl_model(model_3d: Ee3dModel) -> Ki3dModel:
|
||||
def generate_wrl_model(model_3d: Ee3dModel):
|
||||
materials = get_materials(obj_data=model_3d.raw_obj)
|
||||
vertices = get_vertices(obj_data=model_3d.raw_obj)
|
||||
mat_map = map_materials(materials)
|
||||
|
|
@ -297,31 +284,37 @@ def generate_wrl_model(model_3d: Ee3dModel) -> Ki3dModel:
|
|||
|
||||
raw_wrl += shape_str
|
||||
|
||||
return Ki3dModel(
|
||||
translation=None, rotation=None, name=model_3d.name, raw_wrl=raw_wrl
|
||||
)
|
||||
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
|
||||
)
|
||||
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)
|
||||
def export(self, lib_path: str):
|
||||
if self.output is None:
|
||||
return None
|
||||
fname = os.path.join(lib_path, self.output.name+'.wrl')
|
||||
with open(fname, "w", encoding="utf-8") as my_lib:
|
||||
my_lib.write(self.output.raw_wrl)
|
||||
return fname
|
||||
|
||||
|
||||
def download_easyeda_3d_model(lcsc_id, dest_path):
|
||||
api = EasyedaApi()
|
||||
data = api.get_cad_data_of_component(lcsc_id)
|
||||
if not data:
|
||||
return None
|
||||
importer = Easyeda3dModelImporter(data, True)
|
||||
exporter = Exporter3dModelKicad(model_3d=importer.output)
|
||||
os.makedirs(dest_path, exist_ok=True)
|
||||
exporter.export(dest_path)
|
||||
|
||||
|
||||
component_id = 'C2895617'
|
||||
download_easyeda_3d_model(component_id, 'test')
|
||||
exit(0)
|
||||
#
|
||||
if False:
|
||||
a = EasyedaApi()
|
||||
|
|
@ -345,7 +338,7 @@ else:
|
|||
exporter = Exporter3dModelKicad(model_3d=c.output)
|
||||
|
||||
os.makedirs('a.3dshapes', exist_ok=True)
|
||||
exporter.export('a')
|
||||
exporter.export('a.3dshapes')
|
||||
if exporter.output:
|
||||
filename = f"{exporter.output.name}.wrl"
|
||||
lib_path = "a.3dshapes"
|
||||
|
|
|
|||
|
|
@ -402,6 +402,7 @@ docker run --rm \
|
|||
--workdir=$(pwd) \
|
||||
--volume="/tmp:/tmp" \
|
||||
--volume="/etc/group:/etc/group:ro" \
|
||||
--volume="/etc/gshadow:/etc/gshadow:ro" \
|
||||
--volume="/etc/timezone:/etc/timezone:ro" \
|
||||
--volume="/home/$USER:/home/$USER:rw" \
|
||||
--volume="/etc/passwd:/etc/passwd:ro" \
|
||||
|
|
|
|||
|
|
@ -0,0 +1,323 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2023 Salvador E. Tropea
|
||||
# Copyright (c) 2023 Instituto Nacional de Tecnología Industrial
|
||||
# Copyright (c) 2022 uPesy Electronics (Alexis)
|
||||
# License: AGPL-3.0
|
||||
# Project: KiBot (formerly KiPlot)
|
||||
# Adapted from: https://github.com/uPesy/easyeda2kicad.py
|
||||
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
import os
|
||||
import requests
|
||||
import re
|
||||
import textwrap
|
||||
from ..misc import W_EEDA3D
|
||||
from .. import log
|
||||
|
||||
logger = log.get_logger()
|
||||
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"
|
||||
# KiCad materials that we try to detect
|
||||
MATERIAL_PIN_01 = {"name": 'PIN-01', "ambient_intensity": 0.271, "shininess": 0.7, "transparency": 0,
|
||||
"diffuse_color": ('0.824', '0.820', '0.781'), "specular_color": ('0.328', '0.258', '0.172')}
|
||||
MATERIAL_PIN_02 = {"name": 'PIN-02', "ambient_intensity": 0.379, "shininess": 0.4, "transparency": 0,
|
||||
"diffuse_color": ('0.859', '0.738', '0.496'), "specular_color": ('0.137', '0.145', '0.184')}
|
||||
MATERIAL_MET_01 = {"name": 'MET-01', "ambient_intensity": 0.250, "shininess": 0.056, "transparency": 0,
|
||||
"diffuse_color": ('0.298', '0.298', '0.298'), "specular_color": ('0.398', '0.398', '0.398')}
|
||||
MATERIAL_EPOXY_04 = {"name": 'IC-BODY-EPOXY-04', "ambient_intensity": 0.293, "shininess": 0.35, "transparency": 0,
|
||||
"diffuse_color": ('0.148', '0.145', '0.145'), "specular_color": ('0.180', '0.168', '0.160')}
|
||||
|
||||
|
||||
class EasyedaApi:
|
||||
def __init__(self):
|
||||
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):
|
||||
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:
|
||||
logger.warning(W_EEDA3D+"Failed EasyEDA request for `{}`, got: {}".format(lcsc_id, api_response))
|
||||
return {}
|
||||
|
||||
return r.json()
|
||||
|
||||
def get_cad_data_of_component(self, lcsc_id):
|
||||
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):
|
||||
r = requests.get(url=ENDPOINT_3D_MODEL.format(uuid=uuid), headers={"User-Agent": self.headers["User-Agent"]})
|
||||
if r.status_code != requests.codes.ok:
|
||||
logger.warning(W_EEDA3D+"Failed to download 3D model data found for EasyEDA uuid: "+uuid)
|
||||
return None
|
||||
return r.content.decode()
|
||||
|
||||
|
||||
def convert_to_mm(dim: 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, lcsc_id, download_raw_3d_model=True):
|
||||
self.input = easyeda_cp_cad_data
|
||||
self.download_raw_3d_model = download_raw_3d_model
|
||||
self.output = self.create_3d_model(lcsc_id)
|
||||
|
||||
def create_3d_model(self, lcsc_id):
|
||||
ee_data = self.input["packageDetail"]["dataStr"]["shape"] if isinstance(self.input, dict) else self.input
|
||||
|
||||
model_3d_info = self.get_3d_model_info(ee_data=ee_data)
|
||||
if model_3d_info:
|
||||
model_3d = 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
|
||||
|
||||
logger.warning(W_EEDA3D+"No 3D model available for `{}`".format(lcsc_id))
|
||||
return None
|
||||
|
||||
def get_3d_model_info(self, ee_data: str):
|
||||
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):
|
||||
|
||||
material_regex = "newmtl .*?endmtl"
|
||||
matches = re.findall(pattern=material_regex, string=obj_data, flags=re.DOTALL)
|
||||
|
||||
materials = {}
|
||||
for c, match in enumerate(matches):
|
||||
material = {"ambient_intensity": 0.2, "shininess": 0.5, "transparency": 0}
|
||||
material["name"] = "MATERIAL_"+str(c+1)
|
||||
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):
|
||||
vertices_regex = "v (.*?)\n"
|
||||
matches = 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 matches]
|
||||
|
||||
|
||||
def map_materials(mats):
|
||||
""" An heuristic to map generic colors to the ones used by KiCad """
|
||||
# Look for grey, black and yellow materials.
|
||||
greys = []
|
||||
blacks = []
|
||||
golden = []
|
||||
for id, mat in mats.items():
|
||||
r, g, b = map(float, mat['diffuse_color'])
|
||||
if abs(r-g) < 0.02 and abs(g-b) < 0.02:
|
||||
# Same RGB components
|
||||
if r < 0.97 and r > 0.6:
|
||||
# In the 0.6 - 0.97 range
|
||||
greys.append(id)
|
||||
if r < 0.3 and r > 0.1:
|
||||
blacks.append(id)
|
||||
elif r > 0.8 and g < 0.8 and g > 0.7 and b < 0.5:
|
||||
golden.append(id)
|
||||
if (r == 0 and g == 0 and b == 0) or (r == 1 and g == 1 and b == 1):
|
||||
mat['transparency'] = 1
|
||||
else:
|
||||
mat['transparency'] = 0
|
||||
mat['diffuse_color'] = ('%5.3f' % r, '%5.3f' % g, '%5.3f' % b)
|
||||
mat['specular_color'] = tuple('%5.3f' % float(c) for c in mat['specular_color'])
|
||||
# Use greys for the pins and metal body
|
||||
c_greys = len(greys)
|
||||
if c_greys == 1:
|
||||
mats[greys[0]] = MATERIAL_PIN_01
|
||||
elif c_greys > 1:
|
||||
# More than one grey, sort by specular level
|
||||
greys = sorted(greys, key=lambda x: mats[x]['specular_color'][0], reverse=True)
|
||||
mats[greys[0]] = MATERIAL_PIN_01
|
||||
mats[greys[1]] = MATERIAL_MET_01
|
||||
# Use black for the plastic body
|
||||
c_blacks = len(blacks)
|
||||
if c_blacks == 1:
|
||||
mats[blacks[0]] = MATERIAL_EPOXY_04
|
||||
elif c_blacks > 1:
|
||||
blacks = sorted(blacks, key=lambda x: mats[x]['diffuse_color'][0], reverse=True)
|
||||
mats[blacks[0]] = MATERIAL_EPOXY_04
|
||||
# for c, b in enumerate(blacks):
|
||||
# mat_map[b] = 'IC-BODY-EPOXY-%02d' % c+1
|
||||
# Use yellow for golden pins
|
||||
if golden:
|
||||
mats[golden[0]] = MATERIAL_PIN_02
|
||||
|
||||
|
||||
def generate_wrl_model(model_3d: Ee3dModel):
|
||||
materials = get_materials(obj_data=model_3d.raw_obj)
|
||||
vertices = get_vertices(obj_data=model_3d.raw_obj)
|
||||
map_materials(materials)
|
||||
|
||||
raw_wrl = VRML_HEADER
|
||||
# Define all the materials
|
||||
for mat in materials.values():
|
||||
mat_str = textwrap.dedent(
|
||||
f"""
|
||||
Shape {{
|
||||
appearance Appearance {{
|
||||
material DEF {mat['name']} Material {{
|
||||
ambientIntensity {mat['ambient_intensity']}
|
||||
diffuseColor {' '.join(mat['diffuse_color'])}
|
||||
specularColor {' '.join(mat['specular_color'])}
|
||||
shininess {mat['shininess']}
|
||||
transparency {mat['transparency']}
|
||||
}}
|
||||
}}
|
||||
}}"""
|
||||
)
|
||||
raw_wrl += mat_str
|
||||
# Define the shapes
|
||||
shapes = model_3d.raw_obj.split("usemtl")[1:]
|
||||
for shape in shapes:
|
||||
lines = shape.splitlines()
|
||||
material_id = materials[lines[0].replace(" ", "")]["name"]
|
||||
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_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):
|
||||
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, fname=None):
|
||||
if self.output is None:
|
||||
return None
|
||||
if fname is None:
|
||||
fname = self.output.name+'.wrl'
|
||||
fname = os.path.join(lib_path, fname)
|
||||
with open(fname, "w", encoding="utf-8") as my_lib:
|
||||
my_lib.write(self.output.raw_wrl)
|
||||
return fname
|
||||
|
||||
|
||||
def download_easyeda_3d_model(lcsc_id, dest_path, fname=None):
|
||||
api = EasyedaApi()
|
||||
data = api.get_cad_data_of_component(lcsc_id)
|
||||
if not data:
|
||||
return None
|
||||
importer = Easyeda3dModelImporter(data, lcsc_id)
|
||||
exporter = Exporter3dModelKicad(model_3d=importer.output)
|
||||
os.makedirs(dest_path, exist_ok=True)
|
||||
return exporter.export(dest_path, fname)
|
||||
|
|
@ -252,6 +252,7 @@ W_NOPCB3DTL = '(W112) '
|
|||
W_BADPCB3DTXT = '(W113) '
|
||||
W_UNKPCB3DNAME = '(W114) '
|
||||
W_BADPCB3DSTK = '(W115) '
|
||||
W_EEDA3D = '(W116) '
|
||||
# Somehow arbitrary, the colors are real, but can be different
|
||||
PCB_MAT_COLORS = {'fr1': "937042", 'fr2': "949d70", 'fr3': "adacb4", 'fr4': "332B16", 'fr5': "6cc290"}
|
||||
PCB_FINISH_COLORS = {'hal': "8b898c", 'hasl': "8b898c", 'imag': "8b898c", 'enig': "cfb96e", 'enepig': "cfb96e",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
from fnmatch import fnmatch
|
||||
import os
|
||||
import requests
|
||||
from .EasyEDA.easyeda_3d import download_easyeda_3d_model
|
||||
from .misc import W_MISS3D, W_FAILDL, W_DOWN3D, DISABLE_3D_MODEL_TEXT
|
||||
from .gs import GS
|
||||
from .optionable import Optionable
|
||||
|
|
@ -85,15 +86,6 @@ class Base3DOptions(VariantOptions):
|
|||
|
||||
def download_model(self, url, fname, rel_dirs):
|
||||
""" Download the 3D model from the provided URL """
|
||||
# Find a place to store the downloaded model
|
||||
if self._tmp_dir is None:
|
||||
self._tmp_dir = os.environ.get('KIBOT_3D_MODELS')
|
||||
if self._tmp_dir is None:
|
||||
self._tmp_dir = os.path.join(os.path.expanduser('~'), '.cache', 'kibot', '3d')
|
||||
else:
|
||||
self._tmp_dir = os.path.abspath(self._tmp_dir)
|
||||
rel_dirs.append(self._tmp_dir)
|
||||
logger.debug('Using `{}` as dir for downloaded 3D models'.format(self._tmp_dir))
|
||||
dest = os.path.join(self._tmp_dir, fname)
|
||||
os.makedirs(os.path.dirname(dest), exist_ok=True)
|
||||
# Is already there?
|
||||
|
|
@ -126,7 +118,57 @@ class Base3DOptions(VariantOptions):
|
|||
return nm
|
||||
return name
|
||||
|
||||
def download_models(self, rename_filter=None, rename_function=None, rename_data=None, force_wrl=False):
|
||||
def try_download_kicad(self, model, full_name, downloaded, rel_dirs, force_wrl):
|
||||
if not (model.startswith('${KISYS3DMOD}/') or model.startswith('${KICAD6_3DMODEL_DIR}/')):
|
||||
return None
|
||||
# This is a model from KiCad, try to download it
|
||||
fname = model[model.find('/')+1:]
|
||||
replace = None
|
||||
if full_name in downloaded:
|
||||
# Already downloaded
|
||||
return os.path.join(self._tmp_dir, fname)
|
||||
# Download the model
|
||||
url = self.kicad_3d_url+fname
|
||||
replace = self.download_model(url, fname, rel_dirs)
|
||||
if not replace:
|
||||
return None
|
||||
# Successfully downloaded
|
||||
downloaded.add(full_name)
|
||||
# If this is a .wrl also download the .step
|
||||
if url.endswith('.wrl'):
|
||||
url = url[:-4]+'.step'
|
||||
fname = fname[:-4]+'.step'
|
||||
self.download_model(url, fname, rel_dirs)
|
||||
elif force_wrl: # This should be a .step, so we download the wrl
|
||||
url = os.path.splitext(url)[0]+'.wrl'
|
||||
fname = os.path.splitext(fname)[0]+'.wrl'
|
||||
self.download_model(url, fname, rel_dirs)
|
||||
return replace
|
||||
|
||||
def try_download_easyeda(self, model, full_name, downloaded, sch_comp, lcsc_field):
|
||||
if not lcsc_field or not sch_comp:
|
||||
return None
|
||||
lcsc_id = sch_comp.get_field_value(lcsc_field)
|
||||
if not lcsc_id:
|
||||
return None
|
||||
fname = os.path.basename(model)
|
||||
cache_name = os.path.join(self._tmp_dir, fname)
|
||||
if full_name in downloaded:
|
||||
# Already downloaded
|
||||
return cache_name
|
||||
if os.path.isfile(cache_name):
|
||||
downloaded.add(full_name)
|
||||
logger.debug('Using cached model `{}`'.format(cache_name))
|
||||
return cache_name
|
||||
logger.debug('- Trying to download {} component as {}/{}'.format(lcsc_id, self._tmp_dir, fname))
|
||||
replace = download_easyeda_3d_model(lcsc_id, self._tmp_dir, fname)
|
||||
if not replace:
|
||||
return None
|
||||
# Successfully downloaded
|
||||
downloaded.add(full_name)
|
||||
return replace
|
||||
|
||||
def download_models(self, rename_filter=None, rename_function=None, rename_data=None, force_wrl=False, all_comps=None):
|
||||
""" Check we have the 3D models.
|
||||
Inform missing models.
|
||||
Try to download the missing models
|
||||
|
|
@ -141,9 +183,26 @@ class Base3DOptions(VariantOptions):
|
|||
is_copy_mode = rename_filter is not None
|
||||
rel_dirs = getattr(rename_data, 'rel_dirs', [])
|
||||
extra_debug = GS.debug_level > 3
|
||||
# Get a list of components in the schematic. Enables downloading LCSC parts.
|
||||
if all_comps is None and GS.sch_file:
|
||||
GS.load_sch()
|
||||
all_comps = GS.sch.get_components()
|
||||
all_comps_hash = {c.ref: c for c in all_comps}
|
||||
# Find the LCSC field
|
||||
lcsc_field = self.solve_field_name('_field_lcsc_part', empty_when_none=True)
|
||||
# Find a place to store the downloaded models
|
||||
if self._tmp_dir is None:
|
||||
self._tmp_dir = os.environ.get('KIBOT_3D_MODELS')
|
||||
if self._tmp_dir is None:
|
||||
self._tmp_dir = os.path.join(os.path.expanduser('~'), '.cache', 'kibot', '3d')
|
||||
else:
|
||||
self._tmp_dir = os.path.abspath(self._tmp_dir)
|
||||
rel_dirs.append(self._tmp_dir)
|
||||
logger.debug('Using `{}` as dir for downloaded 3D models'.format(self._tmp_dir))
|
||||
# Look for all the footprints
|
||||
for m in GS.get_modules():
|
||||
ref = m.GetReference()
|
||||
sch_comp = all_comps_hash.get(ref, None)
|
||||
# Extract the models (the iterator returns copies)
|
||||
models = m.Models()
|
||||
models_l = []
|
||||
|
|
@ -164,30 +223,10 @@ class Base3DOptions(VariantOptions):
|
|||
if not os.path.isfile(full_name):
|
||||
logger.debugl(2, 'Missing 3D model file {} ({})'.format(full_name, m3d.m_Filename))
|
||||
# Missing 3D model
|
||||
if self.download and (m3d.m_Filename.startswith('${KISYS3DMOD}/') or
|
||||
m3d.m_Filename.startswith('${KICAD6_3DMODEL_DIR}/')):
|
||||
# This is a model from KiCad, try to download it
|
||||
fname = m3d.m_Filename[m3d.m_Filename.find('/')+1:]
|
||||
replace = None
|
||||
if full_name in downloaded:
|
||||
# Already downloaded
|
||||
replace = os.path.join(self._tmp_dir, fname)
|
||||
else:
|
||||
# Download the model
|
||||
url = self.kicad_3d_url+fname
|
||||
replace = self.download_model(url, fname, rel_dirs)
|
||||
if replace:
|
||||
# Successfully downloaded
|
||||
downloaded.add(full_name)
|
||||
# If this is a .wrl also download the .step
|
||||
if url.endswith('.wrl'):
|
||||
url = url[:-4]+'.step'
|
||||
fname = fname[:-4]+'.step'
|
||||
self.download_model(url, fname, rel_dirs)
|
||||
elif force_wrl: # This should be a .step, so we download the wrl
|
||||
url = os.path.splitext(url)[0]+'.wrl'
|
||||
fname = os.path.splitext(fname)[0]+'.wrl'
|
||||
self.download_model(url, fname, rel_dirs)
|
||||
if self.download:
|
||||
replace = self.try_download_kicad(m3d.m_Filename, full_name, downloaded, rel_dirs, force_wrl)
|
||||
if replace is None:
|
||||
replace = self.try_download_easyeda(m3d.m_Filename, full_name, downloaded, sch_comp, lcsc_field)
|
||||
if replace:
|
||||
source_models.add(replace)
|
||||
old_name = m3d.m_Filename
|
||||
|
|
@ -244,7 +283,7 @@ class Base3DOptions(VariantOptions):
|
|||
return ret
|
||||
return GS.pcb_file
|
||||
self.filter_pcb_components(do_3D=True, do_2D=True, highlight=highlight)
|
||||
self.download_models(force_wrl=force_wrl)
|
||||
self.download_models(force_wrl=force_wrl, all_comps=self._comps)
|
||||
fname = self.save_tmp_board()
|
||||
self.unfilter_pcb_components(do_3D=True, do_2D=True)
|
||||
return fname
|
||||
|
|
|
|||
Loading…
Reference in New Issue