ecadlib 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ecadlib/__init__.py +3 -0
- ecadlib/__main__.py +5 -0
- ecadlib/a365_sync.py +637 -0
- ecadlib/agent.py +1407 -0
- ecadlib/agent_queue.py +780 -0
- ecadlib/agent_watch.py +732 -0
- ecadlib/altium365.py +385 -0
- ecadlib/altium_dblib_sync.py +530 -0
- ecadlib/altium_export.py +209 -0
- ecadlib/altium_forms.py +768 -0
- ecadlib/altium_lib.py +1089 -0
- ecadlib/altium_menu.py +1751 -0
- ecadlib/altium_pkg.py +396 -0
- ecadlib/altium_project.py +1095 -0
- ecadlib/app.py +1458 -0
- ecadlib/approvals.py +395 -0
- ecadlib/assets/demo_bom.csv +8 -0
- ecadlib/assets/kicad_action_plugin.py +607 -0
- ecadlib/assets/kicad_reread.py +60 -0
- ecadlib/assets/pcblib_template.json.gz +0 -0
- ecadlib/bom_inputs.py +339 -0
- ecadlib/bom_triage.py +519 -0
- ecadlib/cad_model.py +528 -0
- ecadlib/cfb.py +171 -0
- ecadlib/cli.py +185 -0
- ecadlib/clihelp.py +149 -0
- ecadlib/cliutil.py +119 -0
- ecadlib/completion.py +173 -0
- ecadlib/costs.py +276 -0
- ecadlib/crosscheck.py +412 -0
- ecadlib/datasheet.py +2168 -0
- ecadlib/dedupe.py +572 -0
- ecadlib/detect.py +170 -0
- ecadlib/doctor.py +450 -0
- ecadlib/ds_route.py +541 -0
- ecadlib/eagle_export.py +1409 -0
- ecadlib/easy_setup.py +338 -0
- ecadlib/easyeda_convert.py +386 -0
- ecadlib/easyeda_pro_export.py +972 -0
- ecadlib/eval/__init__.py +0 -0
- ecadlib/eval/baselines/kicad-v1.json +404 -0
- ecadlib/eval/build_set.py +299 -0
- ecadlib/eval/cli.py +119 -0
- ecadlib/eval/gate.py +358 -0
- ecadlib/eval/golden/demo.txt +379 -0
- ecadlib/eval/golden/specs.txt +3120 -0
- ecadlib/eval/golden/testpack.txt +463 -0
- ecadlib/eval/golden.py +446 -0
- ecadlib/eval/replay/kicad-v1.json.gz +0 -0
- ecadlib/eval/runner.py +456 -0
- ecadlib/eval/score.py +179 -0
- ecadlib/eval/sets/kicad-v1.json +630 -0
- ecadlib/firstrun.py +400 -0
- ecadlib/footprint_adv.py +1006 -0
- ecadlib/footprint_bench.py +218 -0
- ecadlib/footprint_check.py +562 -0
- ecadlib/footprint_gen.py +582 -0
- ecadlib/footprint_rules.py +1092 -0
- ecadlib/forge.py +267 -0
- ecadlib/fp_share.py +95 -0
- ecadlib/fp_tidy.py +389 -0
- ecadlib/fsutil.py +62 -0
- ecadlib/house_style.py +1051 -0
- ecadlib/httpcache.py +124 -0
- ecadlib/hub.py +1490 -0
- ecadlib/hub_copy.py +161 -0
- ecadlib/hub_tools.py +79 -0
- ecadlib/kicad_dblib.py +257 -0
- ecadlib/kicad_export.py +837 -0
- ecadlib/kicad_import.py +913 -0
- ecadlib/kicad_index.py +160 -0
- ecadlib/kicad_menu.py +193 -0
- ecadlib/kicad_pcm.py +475 -0
- ecadlib/launcher.py +156 -0
- ecadlib/librarian.py +963 -0
- ecadlib/lifecycle.py +691 -0
- ecadlib/llm/__init__.py +276 -0
- ecadlib/llm/cli.py +70 -0
- ecadlib/llm/openai_compat.py +214 -0
- ecadlib/llm/profiles.py +245 -0
- ecadlib/llm/rails.py +570 -0
- ecadlib/llm/run.py +126 -0
- ecadlib/llm/sampling.py +49 -0
- ecadlib/mcp_server.py +723 -0
- ecadlib/messages.py +419 -0
- ecadlib/migrate.py +407 -0
- ecadlib/notify.py +329 -0
- ecadlib/package_schema.json +1238 -0
- ecadlib/package_specs/CP_EIA-3216-18_Kemet-A.json +27 -0
- ecadlib/package_specs/C_0402_1005Metric.json +22 -0
- ecadlib/package_specs/C_0805_2012Metric.json +22 -0
- ecadlib/package_specs/DFN-8-1EP_3x3mm_P0.5mm_EP1.66x2.38mm.json +40 -0
- ecadlib/package_specs/D_SMA.json +27 -0
- ecadlib/package_specs/D_SMB.json +27 -0
- ecadlib/package_specs/D_SOD-123.json +43 -0
- ecadlib/package_specs/D_SOD-323.json +43 -0
- ecadlib/package_specs/LED_0603_1608Metric.json +23 -0
- ecadlib/package_specs/LED_0805_2012Metric.json +23 -0
- ecadlib/package_specs/LQFP-48_7x7mm_P0.5mm.json +31 -0
- ecadlib/package_specs/L_0805_2012Metric.json +22 -0
- ecadlib/package_specs/MSOP-8_3x3mm_P0.65mm.json +31 -0
- ecadlib/package_specs/QFN-16-1EP_3x3mm_P0.5mm_EP1.75x1.75mm.json +39 -0
- ecadlib/package_specs/QFN-32-1EP_5x5mm_P0.5mm_EP3.45x3.45mm.json +37 -0
- ecadlib/package_specs/R_0402_1005Metric.json +22 -0
- ecadlib/package_specs/R_0603_1608Metric.json +22 -0
- ecadlib/package_specs/R_0805_2012Metric.json +22 -0
- ecadlib/package_specs/R_1206_3216Metric.json +22 -0
- ecadlib/package_specs/SOIC-16_3.9x9.9mm_P1.27mm.json +32 -0
- ecadlib/package_specs/SOIC-8_3.9x4.9mm_P1.27mm.json +32 -0
- ecadlib/package_specs/SOT-223-3_TabPin2.json +56 -0
- ecadlib/package_specs/SOT-23-5.json +57 -0
- ecadlib/package_specs/SOT-23-6.json +62 -0
- ecadlib/package_specs/SOT-23.json +48 -0
- ecadlib/package_specs/TSSOP-16_4.4x5mm_P0.65mm.json +31 -0
- ecadlib/package_specs/VQFN-16-1EP_3x3mm_P0.5mm_EP1.68x1.68mm.json +39 -0
- ecadlib/package_specs/advanced/BGA-100_11.0x11.0mm_Layout10x10_P1.0mm_Ball0.5mm_Pad0.4mm_NSMD.json +26 -0
- ecadlib/package_specs/advanced/BGA-121_9.0x9.0mm_Layout11x11_P0.8mm_Ball0.4mm_Pad0.35mm_NSMD.json +26 -0
- ecadlib/package_specs/advanced/BGA-144_7.0x7.0mm_Layout13x13_P0.5mm_Ball0.3mm_Pad0.25mm_NSMD.json +29 -0
- ecadlib/package_specs/advanced/BGA-169_11.0x11.0mm_Layout13x13_P0.8mm_Ball0.5mm_Pad0.4mm_NSMD.json +23 -0
- ecadlib/package_specs/advanced/BGA-256_11.0x11.0mm_Layout20x20_P0.5mm_Ball0.3mm_Pad0.25mm_NSMD.json +26 -0
- ecadlib/package_specs/advanced/BGA-256_17.0x17.0mm_Layout16x16_P1.0mm_Ball0.5mm_Pad0.4mm_NSMD.json +26 -0
- ecadlib/package_specs/advanced/BGA-324_15.0x15.0mm_Layout18x18_P0.8mm_Ball0.5mm_Pad0.4mm_NSMD.json +26 -0
- ecadlib/package_specs/advanced/BGA-36_3.396x3.466mm_Layout6x6_P0.4mm_Ball0.25mm_Pad0.2mm_NSMD.json +24 -0
- ecadlib/package_specs/advanced/BGA-672_27.0x27.0mm_Layout26x26_P1.0mm_Ball0.6mm_Pad0.5mm_NSMD.json +32 -0
- ecadlib/package_specs/advanced/BGA-68_5.0x5.0mm_Layout9x9_P0.5mm_Ball0.3mm_Pad0.25mm_NSMD.json +30 -0
- ecadlib/package_specs/advanced/BGA-90_8.0x13.0mm_Layout2x3x15_P0.8mm.json +26 -0
- ecadlib/package_specs/advanced/DFN-8-1EP_3x3mm_P0.5mm_EP1.65x2.38mm_ThermalVias.json +60 -0
- ecadlib/package_specs/advanced/Example_Custom_LPad_Slot.json +24 -0
- ecadlib/package_specs/advanced/JST_GH_SM04B-GHS-TB_1x04-1MP_P1.25mm_Horizontal.json +49 -0
- ecadlib/package_specs/advanced/JST_PH_B4B-PH-K_1x04_P2.00mm_Vertical.json +33 -0
- ecadlib/package_specs/advanced/JST_PH_S4B-PH-SM4-TB_1x04-1MP_P2.00mm_Horizontal.json +55 -0
- ecadlib/package_specs/advanced/JST_SH_SM04B-SRSS-TB_1x04-1MP_P1.00mm_Horizontal.json +49 -0
- ecadlib/package_specs/advanced/JST_XH_B2B-XH-A_1x02_P2.50mm_Vertical.json +34 -0
- ecadlib/package_specs/advanced/JST_XH_B4B-XH-A_1x04_P2.50mm_Vertical.json +34 -0
- ecadlib/package_specs/advanced/JST_XH_B6B-XH-A_1x06_P2.50mm_Vertical.json +33 -0
- ecadlib/package_specs/advanced/LFBGA-144_10x10mm_Layout12x12_P0.8mm.json +23 -0
- ecadlib/package_specs/advanced/Molex_KK-254_AE-6410-04A_1x04_P2.54mm_Vertical.json +34 -0
- ecadlib/package_specs/advanced/Molex_PicoBlade_53398-0471_1x04-1MP_P1.25mm_Vertical.json +54 -0
- ecadlib/package_specs/advanced/PinHeader_1x05_P1.00mm_Vertical_SMD_Pin1Left.json +14 -0
- ecadlib/package_specs/advanced/PinHeader_1x05_P1.27mm_Vertical_SMD_Pin1Left.json +14 -0
- ecadlib/package_specs/advanced/PinHeader_1x05_P2.00mm_Vertical.json +12 -0
- ecadlib/package_specs/advanced/PinHeader_1x05_P2.00mm_Vertical_SMD_Pin1Left.json +14 -0
- ecadlib/package_specs/advanced/PinHeader_1x05_P2.54mm_Vertical.json +12 -0
- ecadlib/package_specs/advanced/PinHeader_1x05_P2.54mm_Vertical_SMD_Pin1Left.json +25 -0
- ecadlib/package_specs/advanced/PinHeader_1x05_P2.54mm_Vertical_SMD_Pin1Right.json +14 -0
- ecadlib/package_specs/advanced/PinHeader_1x10_P1.00mm_Vertical.json +15 -0
- ecadlib/package_specs/advanced/PinHeader_2x05_P1.00mm_Vertical_SMD.json +13 -0
- ecadlib/package_specs/advanced/PinHeader_2x05_P1.27mm_Vertical.json +18 -0
- ecadlib/package_specs/advanced/PinHeader_2x05_P1.27mm_Vertical_SMD.json +24 -0
- ecadlib/package_specs/advanced/PinHeader_2x05_P2.00mm_Vertical_SMD.json +13 -0
- ecadlib/package_specs/advanced/PinHeader_2x05_P2.54mm_Vertical.json +12 -0
- ecadlib/package_specs/advanced/PinHeader_2x05_P2.54mm_Vertical_SMD.json +24 -0
- ecadlib/package_specs/advanced/PinHeader_2x20_P2.54mm_Vertical.json +12 -0
- ecadlib/package_specs/advanced/QFN-16-1EP_3x3mm_P0.5mm_EP1.75x1.75mm_ThermalVias.json +87 -0
- ecadlib/package_specs/advanced/UFBGA-132_7x7mm_Layout12x12_P0.5mm.json +43 -0
- ecadlib/package_specs/advanced/USB_C_Receptacle_HRO_TYPE-C-31-M-12.json +218 -0
- ecadlib/package_specs/standard.json.gz +0 -0
- ecadlib/parity.py +1204 -0
- ecadlib/parts_export.py +154 -0
- ecadlib/partview.py +215 -0
- ecadlib/plugins.py +231 -0
- ecadlib/providers.py +226 -0
- ecadlib/rebuild.py +471 -0
- ecadlib/release_check.py +437 -0
- ecadlib/reports.py +274 -0
- ecadlib/resolve.py +556 -0
- ecadlib/review_cli.py +292 -0
- ecadlib/rules.py +979 -0
- ecadlib/rules_infer.py +1205 -0
- ecadlib/sexpr.py +149 -0
- ecadlib/sheetsafe.py +65 -0
- ecadlib/sim_models.py +1087 -0
- ecadlib/sourcing.py +1161 -0
- ecadlib/stdparts.py +755 -0
- ecadlib/step_gen.py +929 -0
- ecadlib/symbol_layout.py +798 -0
- ecadlib/team.py +558 -0
- ecadlib/team_setup.py +288 -0
- ecadlib/term.py +420 -0
- ecadlib/term_views.py +64 -0
- ecadlib/testpack.py +334 -0
- ecadlib/ui/__init__.py +1 -0
- ecadlib/ui/__main__.py +3 -0
- ecadlib/ui/server.py +1029 -0
- ecadlib/ui/static/hairline.js +2 -0
- ecadlib/ui/static/index.html +2098 -0
- ecadlib/ui/static/setup.html +294 -0
- ecadlib/upkeep.py +717 -0
- ecadlib/upstream.py +573 -0
- ecadlib/userconfig.py +257 -0
- ecadlib/visual_check.py +1603 -0
- ecadlib/ws_status.py +203 -0
- ecadlib-0.3.0.dist-info/METADATA +2245 -0
- ecadlib-0.3.0.dist-info/RECORD +199 -0
- ecadlib-0.3.0.dist-info/WHEEL +5 -0
- ecadlib-0.3.0.dist-info/entry_points.txt +2 -0
- ecadlib-0.3.0.dist-info/licenses/LICENSE +21 -0
- ecadlib-0.3.0.dist-info/licenses/NOTICE +53 -0
- ecadlib-0.3.0.dist-info/top_level.txt +1 -0
ecadlib/step_gen.py
ADDED
|
@@ -0,0 +1,929 @@
|
|
|
1
|
+
"""Package-shaped 3D models as STEP files (ISO 10303-21, AP214), built from a package spec or a footprint name.
|
|
2
|
+
|
|
3
|
+
python3 -m ecadlib.step_gen ecadlib/package_specs/SOIC-8_3.9x4.9mm_P1.27mm.json -o out/SOIC-8.step [--height MM]
|
|
4
|
+
python3 -m ecadlib.step_gen SOIC-8_3.9x4.9mm_P1.27mm -o out/SOIC-8.step (a footprint name)
|
|
5
|
+
python3 -m ecadlib.step_gen PinHeader_1x04_P2.54mm_Vertical -o out/hdr.step
|
|
6
|
+
python3 -m ecadlib.step_gen --check out/SOIC-8.step [more.step ...] (validate any STEP file)
|
|
7
|
+
|
|
8
|
+
Pure Python, no CAD kernel. Each model is a set of manifold solid B-reps with planar faces (round parts are
|
|
9
|
+
24-sided prisms), styled with STEP colours (dark body, silver leads, light pin-1 / polarity mark). Units mm.
|
|
10
|
+
Placement follows the KiCad library models, so a model lines up with the footprint at offset 0, rotation 0:
|
|
11
|
+
origin at the footprint origin, z = 0 on the board, x right, y up (KiCad footprint y is down), pin 1 where the
|
|
12
|
+
footprint has it (2-terminal parts: pin 1 at -x; dual/quad rows: pin 1 top left; through-hole: pin 1 at 0,0).
|
|
13
|
+
|
|
14
|
+
Families (package_schema.json `package_type`, else parsed from the footprint name):
|
|
15
|
+
chip R/C/L/LED chips: body + two metallised end caps (LED: white body, green cathode mark; MELF: round)
|
|
16
|
+
molded tantalum (CP_EIA), SMA/SMB/SMC, molded 2-terminal: body + terminals folded under it, polarity band
|
|
17
|
+
gullwing SOIC/SSOP/TSSOP/MSOP/SOT-23(-5/-6)/SOT-223/SOD-123/SOD-323: two-tier tapered body, gull-wing leads,
|
|
18
|
+
tab lead (SOT-223), flat leads (lead_type flat, SOT-89); pin-1 dot or polarity band
|
|
19
|
+
qfp LQFP/TQFP/QFP: four rows of gull-wing leads
|
|
20
|
+
qfn QFN/DFN/SON: body, bottom terminals flush with the edge, exposed pad, mould between them
|
|
21
|
+
TH PinHeader_RxC_P2.54/2.00/1.27mm_Vertical, C/CP_Radial_D..mm_P..mm, R/D_Axial / D_DO-35/41/201AD
|
|
22
|
+
bga BGA/LGA/CSP: balls on the footprint_adv grid (depopulated balls left out) under a flat body, A1 dot
|
|
23
|
+
connector connector / custom pad_table specs: box over the body, pins through the plated holes, flat SMD contacts
|
|
24
|
+
Height: --height, else the spec's height_max, else the CP_EIA-xxxx-HH suffix, else HEIGHT below, else the middle
|
|
25
|
+
of release_check.HEIGHTS for the family.
|
|
26
|
+
|
|
27
|
+
check() parses a STEP file (any producer) and verifies: every #reference resolves; each CLOSED_SHELL is closed
|
|
28
|
+
(every edge used by exactly two faces, in opposite directions); loops are connected; face vertices lie on their
|
|
29
|
+
PLANE and the loop turns the right way round it; LINE geometry passes through its vertices; each solid's
|
|
30
|
+
volume is positive; it reports the bounding box and which solids carry a colour.
|
|
31
|
+
kicad_import.add_body(gen_step=True) and the librarian embed these models where they would otherwise
|
|
32
|
+
extrude a plain box (the box stays the last resort)."""
|
|
33
|
+
import argparse, json, math, re, sys
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
|
|
36
|
+
from .footprint_gen import dim
|
|
37
|
+
|
|
38
|
+
SPECS = Path(__file__).with_name("package_specs")
|
|
39
|
+
COL = {"body": (0.1, 0.1, 0.1), "metal": (0.82, 0.82, 0.85), "mark": (0.78, 0.78, 0.78), "mlcc": (0.62, 0.49, 0.33),
|
|
40
|
+
"led": (0.95, 0.95, 0.92), "led_mark": (0.1, 0.6, 0.2), "tant": (0.88, 0.66, 0.12), "tant_mark": (0.45, 0.28, 0.08),
|
|
41
|
+
"gold": (0.86, 0.7, 0.3), "ecap": (0.12, 0.14, 0.32), "resistor": (0.76, 0.66, 0.46), "inductor": (0.25, 0.25, 0.27)}
|
|
42
|
+
MARK_T = 0.01 # pin-1 / polarity mark thickness (the body top sits this much lower)
|
|
43
|
+
SEG = 24 # sides of a "round" prism
|
|
44
|
+
# overall height (mm) when neither the caller nor the spec gives one: first regex on the footprint name wins
|
|
45
|
+
HEIGHT = [(r"^R_0201", 0.26), (r"^R_0402", 0.35), (r"^R_0603", 0.45), (r"^R_0805", 0.5), (r"^R_(1206|1210)", 0.55), (r"^R_", 0.6),
|
|
46
|
+
(r"^C_0201", 0.3), (r"^C_0402", 0.5), (r"^C_0603", 0.8), (r"^C_0805", 1.25), (r"^C_1206", 1.6), (r"^C_", 2.0),
|
|
47
|
+
(r"^L_0402", 0.5), (r"^L_0603", 0.8), (r"^L_0805", 1.0), (r"^L_", 1.2), (r"^LED_0402", 0.4), (r"^LED_0603", 0.6),
|
|
48
|
+
(r"^LED_0805", 0.8), (r"^LED_", 1.1), (r"^CP_EIA", 1.6), (r"^(D_)?SOD-?123", 1.17), (r"^(D_)?SOD-?323", 0.9),
|
|
49
|
+
(r"^(D_)?SOD-?523", 0.6), (r"^(D_)?SM[ABC]\b", 2.3), (r"^SOT-223", 1.8), (r"^SOT-89", 1.5), (r"^SOT-?(323|363|353)", 0.95),
|
|
50
|
+
(r"^SOT-23", 1.1), (r"^(TSSOP|MSOP|VSSOP|HTSSOP|TSOP)", 1.1), (r"^SSOP", 1.75), (r"^(SOIC|SOP|SO-)", 1.75),
|
|
51
|
+
(r"(QFN|DFN|SON)", 0.8), (r"^LQFP", 1.6), (r"^TQFP", 1.2), (r"QFP", 2.0)]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# ---------------------------------------------------------------- vectors
|
|
55
|
+
def _add(a, b): return (a[0] + b[0], a[1] + b[1], a[2] + b[2])
|
|
56
|
+
def _sub(a, b): return (a[0] - b[0], a[1] - b[1], a[2] - b[2])
|
|
57
|
+
def _mul(a, k): return (a[0] * k, a[1] * k, a[2] * k)
|
|
58
|
+
def _dot(a, b): return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
|
|
59
|
+
def _cross(a, b): return (a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0])
|
|
60
|
+
def _len(a): return math.sqrt(_dot(a, a))
|
|
61
|
+
def _unit(a): return _mul(a, 1.0 / _len(a))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _newell(pts):
|
|
65
|
+
n = (0.0, 0.0, 0.0)
|
|
66
|
+
for a, b in zip(pts, pts[1:] + pts[:1]):
|
|
67
|
+
n = _add(n, ((a[1] - b[1]) * (a[2] + b[2]), (a[2] - b[2]) * (a[0] + b[0]), (a[0] - b[0]) * (a[1] + b[1])))
|
|
68
|
+
return n
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _area2(poly):
|
|
72
|
+
return sum(a[0] * b[1] - b[0] * a[1] for a, b in zip(poly, poly[1:] + poly[:1])) / 2
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# ---------------------------------------------------------------- solids
|
|
76
|
+
class Solid:
|
|
77
|
+
"""Closed polyhedron: verts [(x, y, z)], faces [[outer loop, *hole loops]] of vertex indices, each loop
|
|
78
|
+
counter-clockwise seen from outside the solid (holes clockwise)."""
|
|
79
|
+
def __init__(self, name, verts, faces, color):
|
|
80
|
+
self.name, self.verts, self.faces, self.color = name, verts, faces, color
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def prism(poly, depth, origin=(0, 0, 0), u=(1, 0, 0), v=(0, 1, 0), holes=(), name="", color=COL["body"]):
|
|
84
|
+
"""2D polygon (in the u, v frame at origin) extruded by depth along u x v; holes are inner polygons."""
|
|
85
|
+
w = _cross(u, v)
|
|
86
|
+
poly = list(poly) if _area2(poly) > 0 else list(poly)[::-1]
|
|
87
|
+
loops = [poly] + [list(h) if _area2(h) < 0 else list(h)[::-1] for h in holes]
|
|
88
|
+
P = lambda p, d: _add(_add(_add(origin, _mul(u, p[0])), _mul(v, p[1])), _mul(w, d))
|
|
89
|
+
verts, bot, top = [], [], []
|
|
90
|
+
for lp in loops:
|
|
91
|
+
bot.append(list(range(len(verts), len(verts) + len(lp)))); verts += [P(p, 0) for p in lp]
|
|
92
|
+
top.append(list(range(len(verts), len(verts) + len(lp)))); verts += [P(p, depth) for p in lp]
|
|
93
|
+
faces = [[t for t in top], [b[::-1] for b in bot]]
|
|
94
|
+
for b, t in zip(bot, top):
|
|
95
|
+
k = len(b)
|
|
96
|
+
faces += [[[b[i], b[(i + 1) % k], t[(i + 1) % k], t[i]]] for i in range(k)]
|
|
97
|
+
return Solid(name, verts, faces, color)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def box(x0, y0, z0, x1, y1, z1, name="", color=COL["body"]):
|
|
101
|
+
return prism([(x0, y0), (x1, y0), (x1, y1), (x0, y1)], z1 - z0, (0, 0, z0), name=name, color=color)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def loft(rings, zs, name="", color=COL["body"]):
|
|
105
|
+
"""Stack of horizontal polygons with the same vertex count; consecutive rings must have parallel edges
|
|
106
|
+
(offset_poly of one another) so every side quad is planar."""
|
|
107
|
+
rings = [r if _area2(r) > 0 else r[::-1] for r in rings]
|
|
108
|
+
k, verts = len(rings[0]), []
|
|
109
|
+
for r, z in zip(rings, zs):
|
|
110
|
+
verts += [(x, y, z) for x, y in r]
|
|
111
|
+
idx = lambda j, i: j * k + i % k
|
|
112
|
+
faces = [[[idx(len(rings) - 1, i) for i in range(k)]], [[idx(0, i) for i in range(k)][::-1]]]
|
|
113
|
+
for j in range(len(rings) - 1):
|
|
114
|
+
faces += [[[idx(j, i), idx(j, i + 1), idx(j + 1, i + 1), idx(j + 1, i)]] for i in range(k)]
|
|
115
|
+
return Solid(name, verts, faces, color)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def offset_poly(poly, d):
|
|
119
|
+
"""Move every edge of a counter-clockwise polygon inward by d (edges stay parallel)."""
|
|
120
|
+
lines = []
|
|
121
|
+
for a, b in zip(poly, poly[1:] + poly[:1]):
|
|
122
|
+
t = (b[0] - a[0], b[1] - a[1]); L = math.hypot(*t); t = (t[0] / L, t[1] / L)
|
|
123
|
+
nrm = (-t[1], t[0])
|
|
124
|
+
lines.append(((a[0] + nrm[0] * d, a[1] + nrm[1] * d), t))
|
|
125
|
+
out = []
|
|
126
|
+
for (p1, t1), (p2, t2) in zip(lines[-1:] + lines[:-1], lines):
|
|
127
|
+
den = t1[0] * t2[1] - t1[1] * t2[0]
|
|
128
|
+
s = ((p2[0] - p1[0]) * t2[1] - (p2[1] - p1[1]) * t2[0]) / den
|
|
129
|
+
out.append((p1[0] + t1[0] * s, p1[1] + t1[1] * s))
|
|
130
|
+
return out
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def rect(hx, hy, cx=0.0, cy=0.0):
|
|
134
|
+
return [(cx - hx, cy - hy), (cx + hx, cy - hy), (cx + hx, cy + hy), (cx - hx, cy + hy)]
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def circle(r, cx=0.0, cy=0.0, n=SEG):
|
|
138
|
+
return [(cx + r * math.cos(2 * math.pi * i / n), cy + r * math.sin(2 * math.pi * i / n)) for i in range(n)]
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def thick_polyline(pts, t):
|
|
142
|
+
"""Polygon around a 2D polyline, t thick, mitred corners."""
|
|
143
|
+
def nrm(a, b):
|
|
144
|
+
d = (b[0] - a[0], b[1] - a[1]); L = math.hypot(*d)
|
|
145
|
+
return (-d[1] / L, d[0] / L)
|
|
146
|
+
left, right = [], []
|
|
147
|
+
for i, p in enumerate(pts):
|
|
148
|
+
ns = [nrm(pts[i - 1], p)] if i else []
|
|
149
|
+
ns += [nrm(p, pts[i + 1])] if i + 1 < len(pts) else []
|
|
150
|
+
m = (sum(n[0] for n in ns), sum(n[1] for n in ns)); L = math.hypot(*m); m = (m[0] / L, m[1] / L)
|
|
151
|
+
k = (t / 2) / max(0.25, m[0] * ns[0][0] + m[1] * ns[0][1])
|
|
152
|
+
left.append((p[0] + m[0] * k, p[1] + m[1] * k)); right.append((p[0] - m[0] * k, p[1] - m[1] * k))
|
|
153
|
+
return left + right[::-1]
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def side_profile(profile, out_dir, lateral, width, name, color=COL["metal"]):
|
|
157
|
+
"""A lead: profile polygon in (s = distance from the centre along out_dir, z), `width` wide, centred at
|
|
158
|
+
`lateral` (the pin's coordinate along the row: y for out_dir +-x, x for out_dir +-y)."""
|
|
159
|
+
o = (out_dir[0], out_dir[1], 0.0)
|
|
160
|
+
w = _cross(o, (0, 0, 1))
|
|
161
|
+
c = lateral * (w[0] + w[1]) # w is +-x or +-y: the pin's coordinate along w
|
|
162
|
+
return prism(profile, width, _mul(w, c - width / 2), o, (0, 0, 1), name=name, color=color)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def mark_dot(x, y, r, z, name="pin 1 mark"):
|
|
166
|
+
return prism(circle(r, x, y, 8), MARK_T, (0, 0, z), name=name, color=COL["mark"])
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
# ---------------------------------------------------------------- package builders
|
|
170
|
+
def _n(spec, k):
|
|
171
|
+
return dim(spec[k])[1] if spec.get(k) is not None else None
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _kind(name):
|
|
175
|
+
m = re.match(r"(LED|CP|R|C|L|D|F)_", name or "")
|
|
176
|
+
return m.group(1) if m else ""
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def chip(spec, H):
|
|
180
|
+
L, W = _n(spec, "body_length"), _n(spec, "body_width")
|
|
181
|
+
T = _n(spec, "terminal_length") or (L - _n(spec, "inner_span")) / 2
|
|
182
|
+
kind, pol = _kind(spec["name"]), bool(spec.get("polarized"))
|
|
183
|
+
out = []
|
|
184
|
+
if spec.get("lead_type") == "melf": # round body and caps along x
|
|
185
|
+
r = min(W, H) / 2
|
|
186
|
+
cyl = lambda x0, x1, rr, nm, col: prism(circle(rr, 0, r), x1 - x0, (x0, 0, 0), (0, 1, 0), (0, 0, 1), name=nm, color=col)
|
|
187
|
+
out += [cyl(-L / 2, -L / 2 + T, r, "terminal 1", COL["metal"]), cyl(L / 2 - T, L / 2, r, "terminal 2", COL["metal"])]
|
|
188
|
+
bx0, bx1 = -L / 2 + T, L / 2 - T
|
|
189
|
+
if pol:
|
|
190
|
+
band = bx0 + 0.25 * (bx1 - bx0)
|
|
191
|
+
out += [cyl(bx0, band, r * 0.97, "cathode band", COL["mark"]), cyl(band, bx1, r * 0.97, "body", COL["body"])]
|
|
192
|
+
else:
|
|
193
|
+
out.append(cyl(bx0, bx1, r * 0.97, "body", COL["resistor"] if kind == "R" else COL["body"]))
|
|
194
|
+
return out
|
|
195
|
+
e = min(0.025, 0.04 * min(W, H)) # body sits slightly inside the caps
|
|
196
|
+
out += [box(-L / 2, -W / 2, 0, -L / 2 + T, W / 2, H, "terminal 1", COL["metal"]),
|
|
197
|
+
box(L / 2 - T, -W / 2, 0, L / 2, W / 2, H, "terminal 2", COL["metal"])]
|
|
198
|
+
col = {"C": COL["mlcc"], "LED": COL["led"], "L": COL["inductor"]}.get(kind, COL["body"])
|
|
199
|
+
top = H - e - (MARK_T if pol else 0)
|
|
200
|
+
out.append(box(-L / 2 + T, -W / 2 + e, e, L / 2 - T, W / 2 - e, top, "body", col))
|
|
201
|
+
if pol: # cathode (LED, diode) / + mark on the pin-1 end
|
|
202
|
+
x0 = -L / 2 + T
|
|
203
|
+
out.append(box(x0, -W / 2 + 2 * e, top, x0 + 0.3 * (L - 2 * T), W / 2 - 2 * e, top + MARK_T, "polarity mark",
|
|
204
|
+
COL["led_mark"] if kind == "LED" else COL["mark"]))
|
|
205
|
+
return out
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def molded(spec, H):
|
|
209
|
+
L, W = _n(spec, "body_length"), _n(spec, "body_width")
|
|
210
|
+
Wt = _n(spec, "terminal_width") or W
|
|
211
|
+
T = _n(spec, "terminal_length") or (L - _n(spec, "inner_span")) / 2
|
|
212
|
+
tant = _kind(spec["name"]) in ("CP", "C")
|
|
213
|
+
t = 0.1 if tant else 0.15 # terminal sheet thickness
|
|
214
|
+
hl = H * (0.6 if tant else 0.45) # terminal height up the end face
|
|
215
|
+
prof = [(L / 2 - T, 0), (L / 2, 0), (L / 2, hl), (L / 2 - t, hl), (L / 2 - t, t), (L / 2 - T, t)]
|
|
216
|
+
out = [side_profile(prof, (-1, 0), 0, Wt, "terminal 1"), side_profile(prof, (1, 0), 0, Wt, "terminal 2")]
|
|
217
|
+
top = H - (MARK_T if spec.get("polarized") else 0)
|
|
218
|
+
out.append(box(-L / 2 + t, -W / 2, t, L / 2 - t, W / 2, top, "body", COL["tant"] if tant else COL["body"]))
|
|
219
|
+
if spec.get("polarized"):
|
|
220
|
+
x0 = -L / 2 + t + 0.06 * L
|
|
221
|
+
out.append(box(x0, -W / 2 + 0.1, top, x0 + 0.18 * L, W / 2 - 0.1, H, "polarity band", COL["tant_mark"] if tant else COL["mark"]))
|
|
222
|
+
return out
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _ic_body(hx, hy, A1, zlo, zhi, H):
|
|
226
|
+
"""Two-tier moulded body: tapered from the standoff up to the lead band, straight band, tapered to the top."""
|
|
227
|
+
base = rect(hx, hy)
|
|
228
|
+
dt = min(0.1, 0.05 * min(2 * hx, 2 * hy))
|
|
229
|
+
di = min(dt / 2, max(zlo - A1, 0) * 0.15)
|
|
230
|
+
rings, zs = [offset_poly(base, di), base, base, offset_poly(base, dt)], [A1, zlo, zhi, H - MARK_T]
|
|
231
|
+
if di <= 1e-6 or zlo - A1 < 1e-6: # no lower taper (flat leads at the board)
|
|
232
|
+
rings, zs = rings[1:], [A1, zhi, H - MARK_T]
|
|
233
|
+
return loft(rings, zs, "body"), dt
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _lead_path(s_body, s_toe, Lf, zm, c, flat):
|
|
237
|
+
if flat:
|
|
238
|
+
return [(s_body, c / 2), (s_toe, c / 2)]
|
|
239
|
+
s_knee = max(s_toe - Lf, s_body + c)
|
|
240
|
+
s_knee = min(s_knee, s_toe - c)
|
|
241
|
+
s_sh = s_body + 0.35 * max(s_knee - s_body, 0)
|
|
242
|
+
return [(s_body, zm), (s_sh, zm), (s_knee, c / 2), (s_toe, c / 2)]
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _lead_params(spec, H, pitch):
|
|
246
|
+
flat = spec.get("lead_type") == "flat"
|
|
247
|
+
c = 0.25 if spec.get("tab") and not flat else 0.2 if (pitch or 1) >= 1.0 else 0.15
|
|
248
|
+
A1 = 0.0 if flat else 0.1 if H > 1.3 else 0.05
|
|
249
|
+
zm = c / 2 if flat else max(A1 + (H - A1) * 0.3, c + 0.1)
|
|
250
|
+
return flat, c, A1, zm
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def gullwing(spec, H):
|
|
254
|
+
E, E1, D = _n(spec, "lead_span"), _n(spec, "body_width"), _n(spec, "body_length")
|
|
255
|
+
b, Lf = _n(spec, "terminal_width"), _n(spec, "terminal_length")
|
|
256
|
+
pos = spec.get("pin_positions")
|
|
257
|
+
if not pos:
|
|
258
|
+
e, per = spec["pitch"], spec["pin_count"] // 2
|
|
259
|
+
pos = [{"number": str(i + 1), "side": "L", "offset": (i - (per - 1) / 2) * e} for i in range(per)]
|
|
260
|
+
pos += [{"number": str(per + i + 1), "side": "R", "offset": ((per - 1) / 2 - i) * e} for i in range(per)]
|
|
261
|
+
offs = sorted(p["offset"] for p in pos)
|
|
262
|
+
pitch = spec.get("pitch") or min([b2 - a for a, b2 in zip(offs, offs[1:]) if b2 - a > 1e-6] or [1.0])
|
|
263
|
+
flat, c, A1, zm = _lead_params(spec, H, pitch)
|
|
264
|
+
body, dt = _ic_body(E1 / 2, D / 2, A1, zm - c / 2, zm + c / 2, H)
|
|
265
|
+
out = [body]
|
|
266
|
+
path = thick_polyline(_lead_path(E1 / 2, E / 2, Lf, zm, c, flat), c)
|
|
267
|
+
for p in pos:
|
|
268
|
+
out.append(side_profile(path, (-1, 0) if p["side"] == "L" else (1, 0), -p["offset"], b, f"pin {p['number']}"))
|
|
269
|
+
tab = spec.get("tab")
|
|
270
|
+
if tab:
|
|
271
|
+
tl = dim(tab["length"])[1] if tab.get("length") else Lf
|
|
272
|
+
tpath = thick_polyline(_lead_path(E1 / 2, E / 2, tl, zm, c, flat), c)
|
|
273
|
+
out.append(side_profile(tpath, (-1, 0) if tab.get("side", "R") == "L" else (1, 0), -tab.get("offset", 0.0),
|
|
274
|
+
dim(tab["width"])[1], f"tab {tab['number']}"))
|
|
275
|
+
p1 = next((p for p in pos if str(p["number"]) == "1"), pos[0])
|
|
276
|
+
if spec.get("polarized") and len(pos) == 2:
|
|
277
|
+
x0 = -E1 / 2 + dt + 0.05
|
|
278
|
+
out.append(box(x0, -D / 2 + dt + 0.05, H - MARK_T, x0 + 0.2 * E1, D / 2 - dt - 0.05, H, "polarity band", COL["mark"]))
|
|
279
|
+
else:
|
|
280
|
+
r = min(0.3, 0.12 * min(E1, D))
|
|
281
|
+
sx = -1 if p1["side"] == "L" else 1
|
|
282
|
+
sy = -math.copysign(1, p1["offset"]) if abs(p1["offset"]) > 1e-6 else 0
|
|
283
|
+
out.append(mark_dot(sx * (E1 / 2 - dt - 1.6 * r), sy * (D / 2 - dt - 1.6 * r), r, H - MARK_T))
|
|
284
|
+
return out
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _quad_pins(spec):
|
|
288
|
+
"""[(number, side, offset along the side)] in KiCad's order: left top->bottom, bottom, right, top (model y up)."""
|
|
289
|
+
n, e = spec["pin_count"], spec["pitch"]
|
|
290
|
+
nlr = spec.get("pins_left_right", n // 4)
|
|
291
|
+
ntb = spec.get("pins_top_bottom", (n - 2 * nlr) // 2)
|
|
292
|
+
pins, k = [], 1
|
|
293
|
+
for side, cnt, sgn in (("L", nlr, -1), ("B", ntb, 1), ("R", nlr, 1), ("T", ntb, -1)):
|
|
294
|
+
for i in range(cnt):
|
|
295
|
+
pins.append((str(k), side, sgn * (i - (cnt - 1) / 2) * e)); k += 1
|
|
296
|
+
return pins # offset: model y for L/R rows, model x for B/T rows
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
OUT = {"L": (-1, 0), "R": (1, 0), "B": (0, -1), "T": (0, 1)}
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def qfp(spec, H):
|
|
303
|
+
Bx, By = _n(spec, "body_length"), _n(spec, "body_width")
|
|
304
|
+
Ex = _n(spec, "lead_span")
|
|
305
|
+
Ey = dim(spec.get("lead_span_y") or spec["lead_span"])[1]
|
|
306
|
+
b, Lf = _n(spec, "terminal_width"), _n(spec, "terminal_length")
|
|
307
|
+
flat, c, A1, zm = _lead_params(spec, H, spec["pitch"])
|
|
308
|
+
body, dt = _ic_body(Bx / 2, By / 2, A1, zm - c / 2, zm + c / 2, H)
|
|
309
|
+
out = [body]
|
|
310
|
+
px = thick_polyline(_lead_path(Bx / 2, Ex / 2, Lf, zm, c, flat), c)
|
|
311
|
+
py = thick_polyline(_lead_path(By / 2, Ey / 2, Lf, zm, c, flat), c)
|
|
312
|
+
for num, side, off in _quad_pins(spec):
|
|
313
|
+
out.append(side_profile(px if side in "LR" else py, OUT[side], off, b, f"pin {num}"))
|
|
314
|
+
r = min(0.5, 0.08 * min(Bx, By))
|
|
315
|
+
out.append(mark_dot(-(Bx / 2 - dt - 2 * r), By / 2 - dt - 2 * r, r, H - MARK_T))
|
|
316
|
+
return out
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def qfn(spec, H):
|
|
320
|
+
Bx, By = _n(spec, "body_length"), _n(spec, "body_width")
|
|
321
|
+
b, T = _n(spec, "terminal_width"), _n(spec, "terminal_length")
|
|
322
|
+
tt = min(0.2, 0.3 * H) # lead frame thickness
|
|
323
|
+
pins = _quad_pins(spec)
|
|
324
|
+
out, notches = [], {"L": [], "R": [], "B": [], "T": []}
|
|
325
|
+
for num, side, off in pins:
|
|
326
|
+
if side in "LR":
|
|
327
|
+
sx = -1 if side == "L" else 1
|
|
328
|
+
out.append(box(min(sx * Bx / 2, sx * (Bx / 2 - T)), off - b / 2, 0, max(sx * Bx / 2, sx * (Bx / 2 - T)), off + b / 2, tt,
|
|
329
|
+
f"pin {num}", COL["metal"]))
|
|
330
|
+
else:
|
|
331
|
+
sy = -1 if side == "B" else 1
|
|
332
|
+
out.append(box(off - b / 2, min(sy * By / 2, sy * (By / 2 - T)), 0, off + b / 2, max(sy * By / 2, sy * (By / 2 - T)), tt,
|
|
333
|
+
f"pin {num}", COL["metal"]))
|
|
334
|
+
notches[side].append(off)
|
|
335
|
+
# mould between the terminals: the body outline (counter-clockwise) with a notch per terminal
|
|
336
|
+
poly = []
|
|
337
|
+
for side, start, d in (("B", (-Bx / 2, -By / 2), (1, 0)), ("R", (Bx / 2, -By / 2), (0, 1)),
|
|
338
|
+
("T", (Bx / 2, By / 2), (-1, 0)), ("L", (-Bx / 2, By / 2), (0, -1))):
|
|
339
|
+
poly.append(start)
|
|
340
|
+
n_ = (-d[1], d[0]) # inward
|
|
341
|
+
along = sorted(((o - start[0]) * d[0] if side in "BT" else (o - start[1]) * d[1]) for o in notches[side])
|
|
342
|
+
for a in along:
|
|
343
|
+
for s, k in ((a - b / 2, 0), (a - b / 2, T), (a + b / 2, T), (a + b / 2, 0)):
|
|
344
|
+
poly.append((start[0] + d[0] * s + n_[0] * k, start[1] + d[1] * s + n_[1] * k))
|
|
345
|
+
ep = spec.get("exposed_pad")
|
|
346
|
+
holes = []
|
|
347
|
+
if ep:
|
|
348
|
+
ex, ey = dim(ep["length"])[1], dim(ep["width"])[1]
|
|
349
|
+
out.append(box(-ex / 2, -ey / 2, 0, ex / 2, ey / 2, tt, "exposed pad", COL["metal"]))
|
|
350
|
+
holes.append(rect(ex / 2, ey / 2)[::-1])
|
|
351
|
+
out.append(prism(poly, tt, holes=holes, name="mould", color=COL["body"]))
|
|
352
|
+
out.append(box(-Bx / 2, -By / 2, tt, Bx / 2, By / 2, H - MARK_T, "body", COL["body"]))
|
|
353
|
+
r = min(0.25, 0.08 * min(Bx, By))
|
|
354
|
+
out.append(mark_dot(-(Bx / 2 - 2 * r), By / 2 - 2 * r, r, H - MARK_T))
|
|
355
|
+
return out
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def pin_header(spec, H=None):
|
|
359
|
+
rows, cols, p = spec["rows"], spec["cols"], spec["pitch"]
|
|
360
|
+
hb, top, a = {2.54: (2.5, 8.5, 0.64), 2.0: (2.0, 6.0, 0.5), 1.27: (1.5, 4.0, 0.4)}.get(round(p, 2), (p, 3 * p, 0.25 * p))
|
|
361
|
+
pins = [(c * p, -r * p) for r in range(rows) for c in range(cols)]
|
|
362
|
+
holes = [rect(a / 2, a / 2, x, y)[::-1] for x, y in pins]
|
|
363
|
+
outline = [(-p / 2, -(rows - 1) * p - p / 2), ((cols - 1) * p + p / 2, -(rows - 1) * p - p / 2),
|
|
364
|
+
((cols - 1) * p + p / 2, p / 2), (-p / 2, p / 2)]
|
|
365
|
+
out = [prism(outline, hb, holes=holes, name="body", color=COL["body"])]
|
|
366
|
+
out += [box(x - a / 2, y - a / 2, -3.0, x + a / 2, y + a / 2, top, f"pin {i + 1}", COL["gold"]) for i, (x, y) in enumerate(pins)]
|
|
367
|
+
return out
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def radial(spec, H):
|
|
371
|
+
D, P = spec["diameter"], spec["pitch"]
|
|
372
|
+
wd = 0.5 if D <= 6.3 else 0.6
|
|
373
|
+
out = [prism(circle(D / 2, P / 2, 0), H, name="body", color=COL["ecap"] if spec.get("polarized") else COL["mlcc"])]
|
|
374
|
+
out += [box(x - wd / 2, -wd / 2, -3.0, x + wd / 2, wd / 2, 0, f"pin {i + 1}", COL["metal"]) for i, x in enumerate((0.0, P))]
|
|
375
|
+
return out
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def axial(spec, H=None):
|
|
379
|
+
L, D, P = spec["length"], spec["diameter"], spec["pitch"]
|
|
380
|
+
wd = 0.8 if D > 3.5 else 0.6
|
|
381
|
+
x0, x1, zc = P / 2 - L / 2, P / 2 + L / 2, D / 2
|
|
382
|
+
cyl = lambda a, b2, nm, col: prism(circle(D / 2, 0, zc), b2 - a, (a, 0, 0), (0, 1, 0), (0, 0, 1), name=nm, color=col)
|
|
383
|
+
if spec.get("polarized"): # cathode band at the pin-1 end
|
|
384
|
+
out = [cyl(x0, x0 + 0.15 * L, "cathode band", COL["mark"]), cyl(x0 + 0.15 * L, x1, "body", COL["body"])]
|
|
385
|
+
else:
|
|
386
|
+
out = [cyl(x0, x1, "body", COL["resistor"])]
|
|
387
|
+
for i, path in enumerate(([(x0, zc), (0.0, zc), (0.0, -3.0)], [(x1, zc), (P, zc), (P, -3.0)])):
|
|
388
|
+
out.append(prism(thick_polyline(path, wd), wd, (0, wd / 2, 0), (1, 0, 0), (0, 0, 1), name=f"pin {i + 1}", color=COL["metal"]))
|
|
389
|
+
return out
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def bga(spec, H):
|
|
393
|
+
"""BGA / LGA / CSP: balls (8-sided prisms, the collapsed ball height) under a flat body, A1 dot on top.
|
|
394
|
+
Balls sit where footprint_adv puts the pads (same grid, same depopulated balls)."""
|
|
395
|
+
from .footprint_adv import bga_balls
|
|
396
|
+
Bx, By = _n(spec, "body_length"), _n(spec, "body_width")
|
|
397
|
+
e = spec["pitch"]
|
|
398
|
+
ey = spec.get("pitch_y") or e
|
|
399
|
+
rows, cols = spec["rows"], spec["cols"]
|
|
400
|
+
d = _n(spec, "ball_diameter") or 1.25 * _n(spec, "land_diameter")
|
|
401
|
+
lga = spec.get("ball_type") == "lga"
|
|
402
|
+
A1 = 0.05 if lga else min(0.8 * d, 0.5 * H) # standoff: lands are flush, balls collapse to ~80 %
|
|
403
|
+
out = []
|
|
404
|
+
for name, i, j in bga_balls(spec):
|
|
405
|
+
x, y = (j - (cols - 1) / 2) * e, -((i - (rows - 1) / 2) * ey)
|
|
406
|
+
out.append(prism(rect(d / 2, d / 2, x, y) if lga else circle(d / 2, x, y, 8), A1, (0, 0, 0),
|
|
407
|
+
name=f"ball {name}", color=COL["metal"]))
|
|
408
|
+
out.append(box(-Bx / 2, -By / 2, A1, Bx / 2, By / 2, H - MARK_T, "body", COL["body"]))
|
|
409
|
+
r = min(0.4, 0.06 * min(Bx, By))
|
|
410
|
+
out.append(mark_dot(-(Bx / 2 - 2 * r), By / 2 - 2 * r, r, H - MARK_T))
|
|
411
|
+
return out
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def connector(spec, H):
|
|
415
|
+
"""Connector / custom part from its pad table: a box over the body outline, a pin through every plated hole
|
|
416
|
+
(3 mm below the board) and a flat contact on every SMD pad; pin 1 marked on the body top."""
|
|
417
|
+
from .footprint_adv import table_geometry
|
|
418
|
+
pads, body, _, _, _, _ = table_geometry(spec)
|
|
419
|
+
out = []
|
|
420
|
+
if body is None:
|
|
421
|
+
xs = [p["x"] + s * p["w"] / 2 for p in pads for s in (-1, 1)]
|
|
422
|
+
ys = [p["y"] + s * p["h"] / 2 for p in pads for s in (-1, 1)]
|
|
423
|
+
body = (min(xs), min(ys), max(xs), max(ys))
|
|
424
|
+
x0, y0, x1, y1 = body[0], -body[3], body[2], -body[1] # KiCad y down -> y up
|
|
425
|
+
for p in pads:
|
|
426
|
+
if p.get("via") or p["kind"] == "np_thru_hole":
|
|
427
|
+
continue
|
|
428
|
+
x, y = p["x"], -p["y"]
|
|
429
|
+
turn = round((p.get("rot") or 0) / 90) % 2 # pad sizes and drills are in the pad's frame
|
|
430
|
+
if p["kind"] == "thru_hole":
|
|
431
|
+
a = 0.64 * min(p["drill"]) / 0.95 if min(p["drill"]) < 0.95 else 0.64
|
|
432
|
+
hx, hy = (a / 2, (max(p["drill"]) - min(p["drill"]) + a) / 2) if p["drill"][1] > p["drill"][0] else \
|
|
433
|
+
((max(p["drill"]) - min(p["drill"]) + a) / 2, a / 2)
|
|
434
|
+
hx, hy = (hy, hx) if turn else (hx, hy)
|
|
435
|
+
out.append(box(x - hx, y - hy, -3.0, x + hx, y + hy, min(H, 1.0), f"pin {p['num']}", COL["gold"]))
|
|
436
|
+
else:
|
|
437
|
+
w, h = (p["h"], p["w"]) if turn and not p.get("poly") else (p["w"], p["h"])
|
|
438
|
+
out.append(box(x - 0.4 * w, y - 0.4 * h, 0, x + 0.4 * w, y + 0.4 * h, 0.1, f"pin {p['num']}", COL["metal"]))
|
|
439
|
+
out.append(box(x0, y0, 0.1, x1, y1, H - MARK_T, "body", COL["led"] if spec["package_type"] == "connector" else COL["body"]))
|
|
440
|
+
p1 = next((p for p in pads if p["num"] in ("1", "A1")), None)
|
|
441
|
+
if p1:
|
|
442
|
+
mx = min(max(p1["x"], x0 + 0.4), x1 - 0.4)
|
|
443
|
+
my = min(max(-p1["y"], y0 + 0.4), y1 - 0.4)
|
|
444
|
+
out.append(mark_dot(mx, my, 0.25, H - MARK_T))
|
|
445
|
+
return out
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def _header_any(spec, H=None):
|
|
449
|
+
"""pin_header specs from package_schema: SMD headers have their origin at the centre (footprint_adv), not pin 1."""
|
|
450
|
+
out = pin_header(dict(spec, cols=spec.get("cols", 1)), H)
|
|
451
|
+
if spec.get("mount") == "smd":
|
|
452
|
+
dx, dy = -(spec.get("cols", 1) - 1) * spec["pitch"] / 2, (spec["rows"] - 1) * spec["pitch"] / 2
|
|
453
|
+
for so in out: # SMD: no holes in the board, so the posts stop at the board surface
|
|
454
|
+
so.verts = [(x + dx, y + dy, max(z, 0.0) if so.name.startswith("pin ") else z) for x, y, z in so.verts]
|
|
455
|
+
from .footprint_adv import _header
|
|
456
|
+
posts = {so.name: so for so in out if so.name.startswith("pin ")}
|
|
457
|
+
for p in _header(spec, spec.get("density", "B"))[2]: # a foot from each post out to its land
|
|
458
|
+
so = posts.get(f"pin {p['num']}")
|
|
459
|
+
if so is None:
|
|
460
|
+
continue
|
|
461
|
+
xs, ys = [v[0] for v in so.verts], [v[1] for v in so.verts]
|
|
462
|
+
a, px, py = max(xs) - min(xs), (min(xs) + max(xs)) / 2, -p["y"]
|
|
463
|
+
end = p["x"] + (p["w"] / 2 - 0.05) * (1 if p["x"] >= px else -1)
|
|
464
|
+
out.append(box(min(px, end), py - a / 2, 0.0, max(px, end), py + a / 2, a / 2, f"foot {p['num']}", COL["gold"]))
|
|
465
|
+
return out
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
BUILDERS = {"chip": chip, "molded": molded, "gullwing": gullwing, "qfp": qfp, "qfn": qfn,
|
|
469
|
+
"pin_header": _header_any, "radial": radial, "axial": axial, "bga": bga, "connector": connector, "custom": connector}
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
# ---------------------------------------------------------------- footprint names -> spec
|
|
473
|
+
def _d(v):
|
|
474
|
+
return {"nom": v}
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def _chip_spec(name, kind, code):
|
|
478
|
+
L, W = int(code[:2]) / 10, int(code[2:]) / 10
|
|
479
|
+
T = round(min(0.6, max(0.1, 0.2 * L)), 2)
|
|
480
|
+
if kind == "LED":
|
|
481
|
+
return {"name": name, "package_type": "chip", "pin_count": 2, "body_length": _d(L), "body_width": _d(W),
|
|
482
|
+
"terminal_length": _d(T), "polarized": True}
|
|
483
|
+
return {"name": name, "package_type": "chip", "pin_count": 2, "body_length": _d(L), "body_width": _d(W), "terminal_length": _d(T)}
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
# common packages that have no file in package_specs (typical JEDEC outlines)
|
|
487
|
+
NAMED = {
|
|
488
|
+
"D_SMC": {"package_type": "molded", "body_length": _d(7.0), "body_width": _d(5.9), "terminal_width": _d(3.1), "terminal_length": _d(1.1),
|
|
489
|
+
"polarized": True},
|
|
490
|
+
"D_SOD-523": {"package_type": "gullwing", "lead_span": _d(1.6), "body_width": _d(1.2), "body_length": _d(0.8), "terminal_width": _d(0.3),
|
|
491
|
+
"terminal_length": _d(0.2), "lead_type": "flat", "polarized": True,
|
|
492
|
+
"pin_positions": [{"number": "1", "side": "L", "offset": 0}, {"number": "2", "side": "R", "offset": 0}]},
|
|
493
|
+
"SOT-23-6": {"package_type": "gullwing", "lead_span": _d(2.8), "body_width": _d(1.6), "body_length": _d(2.9), "terminal_width": _d(0.4),
|
|
494
|
+
"terminal_length": _d(0.45), "pitch": 0.95, "pin_count": 6},
|
|
495
|
+
"SOT-323_SC-70": {"package_type": "gullwing", "lead_span": _d(2.1), "body_width": _d(1.25), "body_length": _d(2.0), "terminal_width": _d(0.3),
|
|
496
|
+
"terminal_length": _d(0.3), "pin_positions": [{"number": "1", "side": "L", "offset": -0.65},
|
|
497
|
+
{"number": "2", "side": "L", "offset": 0.65}, {"number": "3", "side": "R", "offset": 0}]},
|
|
498
|
+
"SOT-363_SC-70-6": {"package_type": "gullwing", "lead_span": _d(2.1), "body_width": _d(1.25), "body_length": _d(2.0),
|
|
499
|
+
"terminal_width": _d(0.25), "terminal_length": _d(0.3), "pitch": 0.65, "pin_count": 6},
|
|
500
|
+
"SOT-89-3": {"package_type": "gullwing", "lead_span": _d(4.1), "body_width": _d(2.5), "body_length": _d(4.5), "terminal_width": _d(0.48),
|
|
501
|
+
"terminal_length": _d(0.9), "lead_type": "flat",
|
|
502
|
+
"pin_positions": [{"number": "1", "side": "L", "offset": -1.5}, {"number": "2", "side": "L", "offset": 0},
|
|
503
|
+
{"number": "3", "side": "L", "offset": 1.5}],
|
|
504
|
+
"tab": {"number": "2", "side": "R", "offset": 0, "width": _d(1.7)}},
|
|
505
|
+
}
|
|
506
|
+
GW_SPAN = {3.9: 6.0, 7.5: 10.3, 5.3: 7.8, 4.4: 6.4, 3.0: 4.9, 6.1: 8.1} # body width E1 -> lead span E
|
|
507
|
+
AXIAL = {"DO-35": (4.0, 2.0), "DO-41": (5.2, 2.7), "DO-201AD": (9.5, 5.3), "DO-201": (9.5, 5.3), "DO-15": (7.6, 3.6)}
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def spec_from_name(name):
|
|
511
|
+
"""Footprint name (KiCad style, `Lib:` prefix allowed) -> spec dict, or None when the family is unknown.
|
|
512
|
+
A file in package_specs wins; otherwise the dimensions come from the name and typical JEDEC/EIA outlines."""
|
|
513
|
+
name = name.split(":")[-1]
|
|
514
|
+
f = SPECS / f"{name}.json"
|
|
515
|
+
if f.exists():
|
|
516
|
+
return json.loads(f.read_text())
|
|
517
|
+
key = name if name in NAMED else name[2:] if name.startswith("D_") and name[2:] in NAMED else None
|
|
518
|
+
if key:
|
|
519
|
+
s = dict(NAMED[key], name=name)
|
|
520
|
+
s.setdefault("pin_count", len(s.get("pin_positions", [])) + (1 if s.get("tab") else 0) or 2)
|
|
521
|
+
return s
|
|
522
|
+
m = re.match(r"(R|C|L|LED|D|F|Fuse)_(\d{4})_(\d{4})Metric", name)
|
|
523
|
+
if m:
|
|
524
|
+
return _chip_spec(name, m.group(1), m.group(3))
|
|
525
|
+
m = re.match(r"CP_EIA-(\d{4})-(\d{2})", name)
|
|
526
|
+
if m:
|
|
527
|
+
L, W = int(m.group(1)[:2]) / 10, int(m.group(1)[2:]) / 10
|
|
528
|
+
return {"name": name, "package_type": "molded", "pin_count": 2, "body_length": _d(L), "body_width": _d(W),
|
|
529
|
+
"terminal_width": _d(round(0.7 * W, 2)), "terminal_length": _d(round(min(1.3, 0.25 * L), 2)), "polarized": True,
|
|
530
|
+
"height_max": int(m.group(2)) / 10}
|
|
531
|
+
m = re.match(r"(SOIC|SO|SOP|SSOP|TSSOP|MSOP|VSSOP|HTSSOP|TSOP|QSOP)-(\d+)(?:-1EP)?_([\d.]+)x([\d.]+)mm_P([\d.]+)mm", name)
|
|
532
|
+
if m:
|
|
533
|
+
n, E1, D, e = int(m.group(2)), float(m.group(3)), float(m.group(4)), float(m.group(5))
|
|
534
|
+
return {"name": name, "package_type": "gullwing", "pin_count": n, "pitch": e, "lead_span": _d(GW_SPAN.get(E1, E1 + 2.0)),
|
|
535
|
+
"body_width": _d(E1), "body_length": _d(D), "terminal_width": _d(round(min(0.42, 0.33 * e), 3)),
|
|
536
|
+
"terminal_length": _d(0.8 if e >= 1.0 else 0.6)}
|
|
537
|
+
m = re.match(r"([A-Z]*(?:QFN|DFN|SON))-(\d+)(?:-\d*EP)?_([\d.]+)x([\d.]+)mm_P([\d.]+)mm(?:_EP([\d.]+)x([\d.]+)mm)?", name)
|
|
538
|
+
if m:
|
|
539
|
+
n, Bx, By, e = int(m.group(2)), float(m.group(3)), float(m.group(4)), float(m.group(5))
|
|
540
|
+
s = {"name": name, "package_type": "qfn", "pin_count": n, "pitch": e, "body_length": _d(Bx), "body_width": _d(By),
|
|
541
|
+
"terminal_width": _d(round(min(0.3, 0.5 * e), 3)), "terminal_length": _d(0.4)}
|
|
542
|
+
if "QFN" not in m.group(1):
|
|
543
|
+
s.update(pins_left_right=n // 2, pins_top_bottom=0)
|
|
544
|
+
if m.group(6):
|
|
545
|
+
s["exposed_pad"] = {"length": _d(float(m.group(6))), "width": _d(float(m.group(7)))}
|
|
546
|
+
return s
|
|
547
|
+
m = re.match(r"([A-Z]*QFP)-(\d+)(?:-1EP)?_([\d.]+)x([\d.]+)mm_P([\d.]+)mm", name)
|
|
548
|
+
if m:
|
|
549
|
+
n, Bx, By, e = int(m.group(2)), float(m.group(3)), float(m.group(4)), float(m.group(5))
|
|
550
|
+
return {"name": name, "package_type": "qfp", "pin_count": n, "pitch": e, "body_length": _d(Bx), "body_width": _d(By),
|
|
551
|
+
"lead_span": _d(Bx + 2.0), "lead_span_y": _d(By + 2.0), "terminal_width": _d(round(min(0.37, 0.45 * e), 3)),
|
|
552
|
+
"terminal_length": _d(0.6)}
|
|
553
|
+
m = re.match(r"PinHeader_(\d+)x(\d+)_P([\d.]+)mm_Vertical$", name)
|
|
554
|
+
if m:
|
|
555
|
+
return {"name": name, "package_type": "pin_header", "cols": int(m.group(1)), "rows": int(m.group(2)), "pitch": float(m.group(3))}
|
|
556
|
+
m = re.match(r"(CP|C)_Radial_D([\d.]+)mm(?:_H([\d.]+)mm)?_P([\d.]+)mm", name)
|
|
557
|
+
if m:
|
|
558
|
+
D = float(m.group(2))
|
|
559
|
+
H = float(m.group(3)) if m.group(3) else 11.0 if D <= 6.3 else 11.5 if D <= 8 else 16.0 if D <= 10 else 20.0 if D <= 13 else 25.0
|
|
560
|
+
return {"name": name, "package_type": "radial", "diameter": D, "pitch": float(m.group(4)), "height_max": H,
|
|
561
|
+
"polarized": m.group(1) == "CP"}
|
|
562
|
+
m = re.match(r"(R|C|L|D)_Axial_.*?_L([\d.]+)mm_D([\d.]+)mm_P([\d.]+)mm_Horizontal", name) or \
|
|
563
|
+
re.match(r"(D)_(DO-[\dA-Z]+)\w*?_P([\d.]+)mm_Horizontal", name)
|
|
564
|
+
if m:
|
|
565
|
+
if len(m.groups()) == 4:
|
|
566
|
+
L, D, P = float(m.group(2)), float(m.group(3)), float(m.group(4))
|
|
567
|
+
else:
|
|
568
|
+
key = next((k for k in sorted(AXIAL, key=len, reverse=True) if m.group(2).startswith(k)), None)
|
|
569
|
+
if not key:
|
|
570
|
+
return None
|
|
571
|
+
(L, D), P = AXIAL[key], float(m.group(3))
|
|
572
|
+
return {"name": name, "package_type": "axial", "length": L, "diameter": D, "pitch": P, "polarized": m.group(1) == "D"}
|
|
573
|
+
return None
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
def default_height(spec):
|
|
577
|
+
if spec.get("height_max"):
|
|
578
|
+
return float(spec["height_max"])
|
|
579
|
+
if spec["package_type"] == "pin_header":
|
|
580
|
+
return {2.54: 8.5, 2.0: 6.0, 1.27: 4.0}.get(round(spec["pitch"], 2), 3 * spec["pitch"])
|
|
581
|
+
if spec["package_type"] == "axial":
|
|
582
|
+
return spec["diameter"]
|
|
583
|
+
m = re.match(r"CP_EIA-\d{4}-(\d{2})", spec["name"])
|
|
584
|
+
if m:
|
|
585
|
+
return int(m.group(1)) / 10
|
|
586
|
+
h = next((v for rx, v in HEIGHT if re.search(rx, spec["name"])), None)
|
|
587
|
+
if h is None:
|
|
588
|
+
from .release_check import height_range
|
|
589
|
+
lo, hi, rx = height_range(spec["name"])
|
|
590
|
+
h = round((lo + hi) / 2, 2) if rx else None
|
|
591
|
+
return h
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
# ---------------------------------------------------------------- STEP writer
|
|
595
|
+
def _r(x):
|
|
596
|
+
s = f"{x:.6f}".rstrip("0")
|
|
597
|
+
return "0." if s in ("-0.", "0.") else s
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
def _s(text):
|
|
601
|
+
return "'" + str(text).replace("'", "''").encode("ascii", "replace").decode() + "'"
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
def write_step(solids, name, description="ecadlib generated package model"):
|
|
605
|
+
"""Solids -> STEP AP214 text: one product, one ADVANCED_BREP_SHAPE_REPRESENTATION, colours per solid."""
|
|
606
|
+
ents = []
|
|
607
|
+
def add(s):
|
|
608
|
+
ents.append(s); return len(ents)
|
|
609
|
+
dirs = {}
|
|
610
|
+
def direction(v):
|
|
611
|
+
k = tuple(round(c, 9) + 0.0 for c in v)
|
|
612
|
+
if k not in dirs:
|
|
613
|
+
dirs[k] = add(f"DIRECTION('',({','.join(_r(c) for c in k)}))")
|
|
614
|
+
return dirs[k]
|
|
615
|
+
app = add("APPLICATION_CONTEXT('core data for automotive mechanical design processes')")
|
|
616
|
+
add(f"APPLICATION_PROTOCOL_DEFINITION('international standard','automotive_design',2000,#{app})")
|
|
617
|
+
pctx = add(f"PRODUCT_CONTEXT('',#{app},'mechanical')")
|
|
618
|
+
prod = add(f"PRODUCT({_s(name)},{_s(name)},{_s(description)},(#{pctx}))")
|
|
619
|
+
add(f"PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#{prod}))")
|
|
620
|
+
pdf = add(f"PRODUCT_DEFINITION_FORMATION('','',#{prod})")
|
|
621
|
+
pdc = add(f"PRODUCT_DEFINITION_CONTEXT('part definition',#{app},'design')")
|
|
622
|
+
pd = add(f"PRODUCT_DEFINITION('design','',#{pdf},#{pdc})")
|
|
623
|
+
pds = add(f"PRODUCT_DEFINITION_SHAPE('','',#{pd})")
|
|
624
|
+
mm = add("( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) )")
|
|
625
|
+
rad = add("( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) )")
|
|
626
|
+
sr = add("( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() )")
|
|
627
|
+
unc = add(f"UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#{mm},'distance_accuracy_value','confusion accuracy')")
|
|
628
|
+
ctx = add(f"( GEOMETRIC_REPRESENTATION_CONTEXT(3) GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#{unc})) "
|
|
629
|
+
f"GLOBAL_UNIT_ASSIGNED_CONTEXT((#{mm},#{rad},#{sr})) REPRESENTATION_CONTEXT('Context #1','3D Context with UNIT and UNCERTAINTY') )")
|
|
630
|
+
o = add("CARTESIAN_POINT('',(0.,0.,0.))")
|
|
631
|
+
ax0 = add(f"AXIS2_PLACEMENT_3D('',#{o},#{direction((0, 0, 1))},#{direction((1, 0, 0))})")
|
|
632
|
+
breps, styled, styles = [], [], {}
|
|
633
|
+
for sol in solids:
|
|
634
|
+
cps = [add(f"CARTESIAN_POINT('',({_r(x)},{_r(y)},{_r(z)}))") for x, y, z in sol.verts]
|
|
635
|
+
vps = [add(f"VERTEX_POINT('',#{c})") for c in cps]
|
|
636
|
+
edges, faces = {}, []
|
|
637
|
+
def edge(a, b):
|
|
638
|
+
k = (min(a, b), max(a, b))
|
|
639
|
+
if k not in edges:
|
|
640
|
+
d = _sub(sol.verts[k[1]], sol.verts[k[0]])
|
|
641
|
+
vec = add(f"VECTOR('',#{direction(_unit(d))},{_r(_len(d))})")
|
|
642
|
+
ln = add(f"LINE('',#{cps[k[0]]},#{vec})")
|
|
643
|
+
edges[k] = add(f"EDGE_CURVE('',#{vps[k[0]]},#{vps[k[1]]},#{ln},.T.)")
|
|
644
|
+
return edges[k], ".T." if a < b else ".F."
|
|
645
|
+
for loops in sol.faces:
|
|
646
|
+
bounds = []
|
|
647
|
+
for j, lp in enumerate(loops):
|
|
648
|
+
oes = [add("ORIENTED_EDGE('',*,*,#%d,%s)" % edge(a, b)) for a, b in zip(lp, lp[1:] + lp[:1])]
|
|
649
|
+
el = add(f"EDGE_LOOP('',({','.join(f'#{x}' for x in oes)}))")
|
|
650
|
+
bounds.append(add(f"{'FACE_OUTER_BOUND' if j == 0 else 'FACE_BOUND'}('',#{el},.T.)"))
|
|
651
|
+
outer = [sol.verts[i] for i in loops[0]]
|
|
652
|
+
nrm = _unit(_newell(outer))
|
|
653
|
+
ref = _unit(_sub(outer[1], outer[0]))
|
|
654
|
+
ax = add(f"AXIS2_PLACEMENT_3D('',#{cps[loops[0][0]]},#{direction(nrm)},#{direction(ref)})")
|
|
655
|
+
pl = add(f"PLANE('',#{ax})")
|
|
656
|
+
faces.append(add(f"ADVANCED_FACE('',({','.join(f'#{x}' for x in bounds)}),#{pl},.T.)"))
|
|
657
|
+
shell = add(f"CLOSED_SHELL('',({','.join(f'#{x}' for x in faces)}))")
|
|
658
|
+
breps.append(add(f"MANIFOLD_SOLID_BREP({_s(sol.name)},#{shell})"))
|
|
659
|
+
if sol.color not in styles:
|
|
660
|
+
rgb = add(f"COLOUR_RGB('',{','.join(_r(c) for c in sol.color)})")
|
|
661
|
+
fasc = add(f"FILL_AREA_STYLE_COLOUR('',#{rgb})")
|
|
662
|
+
fas = add(f"FILL_AREA_STYLE('',(#{fasc}))")
|
|
663
|
+
ssfa = add(f"SURFACE_STYLE_FILL_AREA(#{fas})")
|
|
664
|
+
sss = add(f"SURFACE_SIDE_STYLE('',(#{ssfa}))")
|
|
665
|
+
ssu = add(f"SURFACE_STYLE_USAGE(.BOTH.,#{sss})")
|
|
666
|
+
styles[sol.color] = add(f"PRESENTATION_STYLE_ASSIGNMENT((#{ssu}))")
|
|
667
|
+
styled.append(add(f"STYLED_ITEM('color',(#{styles[sol.color]}),#{breps[-1]})"))
|
|
668
|
+
rep = add(f"ADVANCED_BREP_SHAPE_REPRESENTATION({_s(name)},(#{ax0},{','.join(f'#{b}' for b in breps)}),#{ctx})")
|
|
669
|
+
add(f"SHAPE_DEFINITION_REPRESENTATION(#{pds},#{rep})")
|
|
670
|
+
add(f"MECHANICAL_DESIGN_GEOMETRIC_PRESENTATION_REPRESENTATION('',({','.join(f'#{s}' for s in styled)}),#{ctx})")
|
|
671
|
+
head = ["ISO-10303-21;", "HEADER;", f"FILE_DESCRIPTION(({_s(description)}),'2;1');",
|
|
672
|
+
f"FILE_NAME({_s(name + '.step')},'2026-01-01T00:00:00',('ecadlib'),('ecadlib'),'ecadlib step_gen','ecadlib step_gen','');",
|
|
673
|
+
"FILE_SCHEMA(('AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }'));", "ENDSEC;", "DATA;"]
|
|
674
|
+
return "\n".join(head + [f"#{i + 1}={e};" for i, e in enumerate(ents)] + ["ENDSEC;", "END-ISO-10303-21;", ""])
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
# ---------------------------------------------------------------- public entry points
|
|
678
|
+
def build(spec, height=None):
|
|
679
|
+
"""spec dict -> (solids, info). info: family, height, height_src."""
|
|
680
|
+
t = spec["package_type"]
|
|
681
|
+
if t not in BUILDERS:
|
|
682
|
+
raise ValueError(f"no 3D builder for package_type {t!r}")
|
|
683
|
+
H, src = (float(height), "caller") if height else (default_height(spec), "spec/table")
|
|
684
|
+
if not H:
|
|
685
|
+
raise ValueError(f"{spec['name']}: no height (no height_max, no table entry); pass a height")
|
|
686
|
+
solids = BUILDERS[t](spec, H)
|
|
687
|
+
return solids, {"family": t, "height": H, "height_src": src, "solids": len(solids)}
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
def generate(src, height=None, name=None):
|
|
691
|
+
"""spec dict, spec .json path or footprint name -> (STEP text, info). Raises ValueError when the
|
|
692
|
+
footprint family is unknown."""
|
|
693
|
+
if isinstance(src, dict):
|
|
694
|
+
spec = src
|
|
695
|
+
elif str(src).endswith(".json") and Path(src).exists():
|
|
696
|
+
spec = json.loads(Path(src).read_text())
|
|
697
|
+
else:
|
|
698
|
+
spec = spec_from_name(str(src))
|
|
699
|
+
if spec is None:
|
|
700
|
+
raise ValueError(f"{src}: no package spec and the footprint name is not a known package family")
|
|
701
|
+
try:
|
|
702
|
+
solids, info = build(spec, height)
|
|
703
|
+
info["name"] = name or spec["name"]
|
|
704
|
+
text = write_step(solids, info["name"]) # zero-size dimensions fail here (zero-length edge)
|
|
705
|
+
except (KeyError, TypeError, IndexError, ZeroDivisionError) as e: # incomplete spec: no model, not a crash
|
|
706
|
+
raise ValueError(f"{spec.get('name', src)}: spec incomplete for a 3D model ({type(e).__name__}: {e})")
|
|
707
|
+
rep = check(text)
|
|
708
|
+
if rep["errors"]:
|
|
709
|
+
raise ValueError(f"{info['name']}: generated STEP failed its own check: {rep['errors'][:3]}")
|
|
710
|
+
info.update(bbox=rep["bbox"], volume=rep["volume"], bytes=len(text))
|
|
711
|
+
return text, info
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
# ---------------------------------------------------------------- STEP reader / checker
|
|
715
|
+
TOK = re.compile(r"\s*(?:(#\d+)|('(?:[^']|'')*')|(\.[A-Z_0-9]+\.)|([-+]?(?:\d+\.?\d*(?:[Ee][-+]?\d+)?|\.\d+(?:[Ee][-+]?\d+)?))|"
|
|
716
|
+
r"([A-Za-z_][A-Za-z_0-9]*)|([()=,;$*]))")
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
def parse(text):
|
|
720
|
+
"""STEP DATA section -> {id: (TYPE, args)}; complex entities -> ('COMPLEX', [(TYPE, args), ...]).
|
|
721
|
+
args: nested lists; ('ref', n), str (quoted strings keep their quotes), float, ('enum', X), None for $ and *."""
|
|
722
|
+
m = re.search(r"\bDATA\s*;(.*?)\bENDSEC\s*;", text, re.S)
|
|
723
|
+
if not m:
|
|
724
|
+
raise ValueError("no DATA section")
|
|
725
|
+
data = re.sub(r"/\*.*?\*/", "", m.group(1), flags=re.S)
|
|
726
|
+
toks, pos = [], 0
|
|
727
|
+
while pos < len(data):
|
|
728
|
+
mt = TOK.match(data, pos)
|
|
729
|
+
if not mt or mt.end() == pos:
|
|
730
|
+
if data[pos:].strip():
|
|
731
|
+
raise ValueError(f"bad token at {data[pos:pos + 30]!r}")
|
|
732
|
+
break
|
|
733
|
+
pos = mt.end()
|
|
734
|
+
ref, st, en, num, kw, p = mt.groups()
|
|
735
|
+
toks.append(("ref", int(ref[1:])) if ref else ("str", st) if st else ("enum", en[1:-1]) if en else
|
|
736
|
+
("num", float(num)) if num else ("kw", kw) if kw else ("p", p))
|
|
737
|
+
i = 0
|
|
738
|
+
def args():
|
|
739
|
+
nonlocal i
|
|
740
|
+
out = [] # toks[i] is '('
|
|
741
|
+
i += 1
|
|
742
|
+
while toks[i] != ("p", ")"):
|
|
743
|
+
k, v = toks[i]
|
|
744
|
+
if (k, v) == ("p", ","):
|
|
745
|
+
i += 1; continue
|
|
746
|
+
if (k, v) == ("p", "("):
|
|
747
|
+
out.append(args()); continue
|
|
748
|
+
if k == "kw" and toks[i + 1] == ("p", "("):
|
|
749
|
+
i += 1; out.append(("typed", v, args())); continue
|
|
750
|
+
out.append(("ref", v) if k == "ref" else ("enum", v) if k == "enum" else None if k == "p" else v)
|
|
751
|
+
i += 1
|
|
752
|
+
i += 1
|
|
753
|
+
return out
|
|
754
|
+
ents = {}
|
|
755
|
+
while i < len(toks):
|
|
756
|
+
k, v = toks[i]
|
|
757
|
+
if k != "ref" or toks[i + 1] != ("p", "="):
|
|
758
|
+
raise ValueError(f"expected '#n =' at token {i}: {toks[i:i + 3]}")
|
|
759
|
+
eid, i = v, i + 2
|
|
760
|
+
if toks[i] == ("p", "("): # complex entity: ( A(..) B(..) )
|
|
761
|
+
i += 1; parts = []
|
|
762
|
+
while toks[i] != ("p", ")"):
|
|
763
|
+
kw = toks[i][1]; i += 1
|
|
764
|
+
parts.append((kw, args()))
|
|
765
|
+
i += 1
|
|
766
|
+
ents[eid] = ("COMPLEX", parts)
|
|
767
|
+
else:
|
|
768
|
+
kw = toks[i][1]; i += 1
|
|
769
|
+
ents[eid] = (kw, args())
|
|
770
|
+
if toks[i] != ("p", ";"):
|
|
771
|
+
raise ValueError(f"#{eid}: expected ';'")
|
|
772
|
+
i += 1
|
|
773
|
+
return ents
|
|
774
|
+
|
|
775
|
+
|
|
776
|
+
def _refs(a):
|
|
777
|
+
if isinstance(a, tuple) and a and a[0] == "ref":
|
|
778
|
+
yield a[1]
|
|
779
|
+
elif isinstance(a, tuple) and a and a[0] == "typed":
|
|
780
|
+
yield from _refs(a[2])
|
|
781
|
+
elif isinstance(a, list):
|
|
782
|
+
for x in a:
|
|
783
|
+
yield from _refs(x)
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
def check(text, tol=1e-6):
|
|
787
|
+
"""Validate a STEP file's B-rep (see module docstring) -> {errors, solids, bbox, volume, unstyled, ...}."""
|
|
788
|
+
errs = []
|
|
789
|
+
try:
|
|
790
|
+
E = parse(text)
|
|
791
|
+
except (ValueError, IndexError) as e:
|
|
792
|
+
return {"errors": [f"parse: {e}"], "solids": 0, "bbox": None, "volume": 0.0, "unstyled": [], "entities": 0}
|
|
793
|
+
for eid, (t, a) in E.items():
|
|
794
|
+
for r in _refs(a):
|
|
795
|
+
if r not in E:
|
|
796
|
+
errs.append(f"#{eid} {t} refers to missing #{r}")
|
|
797
|
+
if errs:
|
|
798
|
+
return {"errors": errs, "solids": 0, "bbox": None, "volume": 0.0, "unstyled": [], "entities": len(E)}
|
|
799
|
+
try:
|
|
800
|
+
return _check_breps(E, tol)
|
|
801
|
+
except (KeyError, IndexError, TypeError, ValueError, ZeroDivisionError) as e: # wrong entity types / arity
|
|
802
|
+
return {"errors": [f"malformed B-rep: {type(e).__name__}: {e}"], "solids": 0, "bbox": None, "volume": 0.0,
|
|
803
|
+
"unstyled": [], "entities": len(E)}
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
def _check_breps(E, tol):
|
|
807
|
+
errs = []
|
|
808
|
+
ref = lambda x: E[x[1]]
|
|
809
|
+
pt = lambda r: tuple(E[r][1][1]) # CARTESIAN_POINT id -> xyz
|
|
810
|
+
vtx = lambda r: pt(E[r][1][1][1]) # VERTEX_POINT id -> xyz
|
|
811
|
+
def dirv(r):
|
|
812
|
+
t, a = E[r]
|
|
813
|
+
return tuple(a[1])
|
|
814
|
+
breps = [k for k, (t, a) in E.items() if t in ("MANIFOLD_SOLID_BREP", "BREP_WITH_VOIDS")]
|
|
815
|
+
styled = {r for k, (t, a) in E.items() if t in ("STYLED_ITEM", "OVER_RIDING_STYLED_ITEM") for r in [a[2][1]] if isinstance(a[2], tuple)}
|
|
816
|
+
allp, vols, per = [], [], []
|
|
817
|
+
for b in breps:
|
|
818
|
+
sh = E[b][1][1][1]
|
|
819
|
+
if E[sh][0] != "CLOSED_SHELL":
|
|
820
|
+
errs.append(f"#{b}: shell #{sh} is {E[sh][0]}"); continue
|
|
821
|
+
use, vol, pts = {}, 0.0, []
|
|
822
|
+
for fr in E[sh][1][1]:
|
|
823
|
+
fid = fr[1]
|
|
824
|
+
ft, fa = E[fid]
|
|
825
|
+
if ft not in ("ADVANCED_FACE", "FACE_SURFACE"):
|
|
826
|
+
errs.append(f"#{fid}: face type {ft}"); continue
|
|
827
|
+
surf = E[fa[2][1]]
|
|
828
|
+
sense = fa[3] == ("enum", "T")
|
|
829
|
+
plane = None
|
|
830
|
+
if surf[0] == "PLANE":
|
|
831
|
+
ax = E[surf[1][1][1]][1]
|
|
832
|
+
plane = (pt(ax[1][1]), _unit(dirv(ax[2][1])))
|
|
833
|
+
area = (0.0, 0.0, 0.0)
|
|
834
|
+
p0 = None
|
|
835
|
+
for bi, br in enumerate(fa[1]):
|
|
836
|
+
bt, ba = E[br[1]]
|
|
837
|
+
lt, la = E[ba[1][1]]
|
|
838
|
+
if lt != "EDGE_LOOP":
|
|
839
|
+
if lt == "VERTEX_LOOP":
|
|
840
|
+
continue
|
|
841
|
+
errs.append(f"#{br[1]}: loop type {lt}"); continue
|
|
842
|
+
seq = []
|
|
843
|
+
for oe in la[1]:
|
|
844
|
+
ot, oa = E[oe[1]]
|
|
845
|
+
ec = oa[3][1]
|
|
846
|
+
et, ea = E[ec]
|
|
847
|
+
v1, v2 = ea[1][1], ea[2][1]
|
|
848
|
+
fwd = oa[4] == ("enum", "T")
|
|
849
|
+
s, e = (v1, v2) if oa[4] == ("enum", "T") else (v2, v1)
|
|
850
|
+
seq.append((s, e))
|
|
851
|
+
use.setdefault(ec, []).append((fid, fwd))
|
|
852
|
+
g = E[ea[3][1]]
|
|
853
|
+
if g[0] == "LINE":
|
|
854
|
+
lp, d = pt(g[1][1][1]), _unit(dirv(E[g[1][2][1]][1][1][1]))
|
|
855
|
+
for v in (vtx(v1), vtx(v2)):
|
|
856
|
+
if _len(_cross(_sub(v, lp), d)) > 1e-5:
|
|
857
|
+
errs.append(f"#{ec}: vertex {v} not on its LINE"); break
|
|
858
|
+
for (s1, e1), (s2, e2) in zip(seq, seq[1:] + seq[:1]):
|
|
859
|
+
if e1 != s2:
|
|
860
|
+
errs.append(f"#{ba[1][1]}: loop not connected (#{e1} -> #{s2})"); break
|
|
861
|
+
poly = [vtx(s) for s, _ in seq]
|
|
862
|
+
if bt == "FACE_BOUND" and ba[2] == ("enum", "F") or bt == "FACE_OUTER_BOUND" and ba[2] == ("enum", "F"):
|
|
863
|
+
poly = poly[::-1]
|
|
864
|
+
pts += poly
|
|
865
|
+
nw = _newell(poly)
|
|
866
|
+
area = _add(area, nw)
|
|
867
|
+
p0 = p0 or poly[0]
|
|
868
|
+
if plane:
|
|
869
|
+
for v in poly:
|
|
870
|
+
if abs(_dot(_sub(v, plane[0]), plane[1])) > 1e-5:
|
|
871
|
+
errs.append(f"#{fid}: vertex {v} off its plane"); break
|
|
872
|
+
if bi == 0 and _dot(nw, plane[1]) * (1 if sense else -1) <= 0:
|
|
873
|
+
errs.append(f"#{fid}: outer loop runs the wrong way round the face normal")
|
|
874
|
+
if p0 is not None:
|
|
875
|
+
vol += _dot(p0, area) / 6.0 # Newell vector = 2 x area vector
|
|
876
|
+
for ec, us in use.items():
|
|
877
|
+
if len(us) != 2:
|
|
878
|
+
errs.append(f"#{b}: edge #{ec} used by {len(us)} face(s), not 2 (shell not closed)")
|
|
879
|
+
elif us[0][1] == us[1][1]:
|
|
880
|
+
errs.append(f"#{b}: edge #{ec} used twice in the same direction (inconsistent orientation)")
|
|
881
|
+
if vol <= tol:
|
|
882
|
+
errs.append(f"#{b} {E[b][1][0]}: volume {vol:.6g} is not positive")
|
|
883
|
+
vols.append(vol); allp += pts
|
|
884
|
+
per.append({"id": b, "name": E[b][1][0].strip("'"), "volume": round(vol, 6), "faces": len(E[sh][1][1]),
|
|
885
|
+
"styled": b in styled})
|
|
886
|
+
if not breps:
|
|
887
|
+
errs.append("no MANIFOLD_SOLID_BREP")
|
|
888
|
+
bbox = tuple(round(min(p[i] for p in allp), 6) for i in range(3)) + tuple(round(max(p[i] for p in allp), 6) for i in range(3)) if allp else None
|
|
889
|
+
return {"errors": errs, "solids": len(breps), "bbox": bbox, "volume": round(sum(vols), 6), "entities": len(E),
|
|
890
|
+
"unstyled": [s["name"] for s in per if not s["styled"]], "per_solid": per}
|
|
891
|
+
|
|
892
|
+
|
|
893
|
+
# ---------------------------------------------------------------- CLI
|
|
894
|
+
def main(argv=None):
|
|
895
|
+
ap = argparse.ArgumentParser(description="Generate a package-shaped STEP model, or check STEP files")
|
|
896
|
+
ap.add_argument("src", nargs="+", help="package spec .json, footprint name (e.g. SOIC-8_3.9x4.9mm_P1.27mm), or with --check STEP files")
|
|
897
|
+
ap.add_argument("-o", "--out", help="output .step (default <name>.step)")
|
|
898
|
+
ap.add_argument("--height", type=float, help="overall height, mm (default: spec height_max, else a per-package table)")
|
|
899
|
+
ap.add_argument("--check", action="store_true", help="validate the given STEP files instead")
|
|
900
|
+
a = ap.parse_args(argv)
|
|
901
|
+
if a.check:
|
|
902
|
+
bad = 0
|
|
903
|
+
for f in a.src:
|
|
904
|
+
r = check(Path(f).read_text(errors="replace"))
|
|
905
|
+
bb = " x ".join(f"{r['bbox'][i + 3] - r['bbox'][i]:.3f}" for i in range(3)) if r["bbox"] else "-"
|
|
906
|
+
print(f"{'ok ' if not r['errors'] else 'FAIL'} {f}: {r['solids']} solids, volume {r['volume']:.4g} mm3, bbox {bb} mm "
|
|
907
|
+
f"{r['bbox']}" + (f", no colour: {r['unstyled']}" if r["unstyled"] else ""))
|
|
908
|
+
for e in r["errors"][:20]:
|
|
909
|
+
print(f" {e}")
|
|
910
|
+
bad += bool(r["errors"])
|
|
911
|
+
return 1 if bad else 0
|
|
912
|
+
if len(a.src) != 1:
|
|
913
|
+
ap.error("give one spec or footprint name (or --check FILES)")
|
|
914
|
+
try:
|
|
915
|
+
text, info = generate(a.src[0], a.height)
|
|
916
|
+
except ValueError as e:
|
|
917
|
+
print(f"step_gen: {e}", file=sys.stderr)
|
|
918
|
+
return 2
|
|
919
|
+
out = Path(a.out or f"{info['name']}.step")
|
|
920
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
921
|
+
out.write_text(text)
|
|
922
|
+
bb = info["bbox"]
|
|
923
|
+
print(f"{info['name']}: {info['family']}, {info['solids']} solids, height {info['height']} mm ({info['height_src']}), "
|
|
924
|
+
f"bbox x {bb[0]:.3f}..{bb[3]:.3f} y {bb[1]:.3f}..{bb[4]:.3f} z {bb[2]:.3f}..{bb[5]:.3f}, {info['bytes'] // 1024} KiB -> {out}")
|
|
925
|
+
return 0
|
|
926
|
+
|
|
927
|
+
|
|
928
|
+
if __name__ == "__main__":
|
|
929
|
+
sys.exit(main())
|