Added mechanism to download 3D models from EasyEDA

- Using LCSC codes

Closes #380
This commit is contained in:
Salvador E. Tropea 2023-01-31 13:12:06 -03:00
parent 305bfb7d0c
commit df085fa8bc
10 changed files with 547 additions and 83 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

323
kibot/EasyEDA/easyeda_3d.py Normal file
View File

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

View File

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

View File

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