mirror of
https://github.com/diodeinc/pcb.git
synced 2026-09-14 20:17:19 +08:00
755 lines
23 KiB
Python
Executable File
755 lines
23 KiB
Python
Executable File
#!/usr/bin/env -S uv run --script
|
|
# Copyright (c) 2026 Diode
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
# /// script
|
|
# requires-python = ">=3.11"
|
|
# dependencies = [
|
|
# "kicad-library-generators[3d] @ git+https://gitlab.com/kicad/libraries/kicad-library-tools.git@5ff1b4f05a5addf83bbe5ce4581426f0888273db",
|
|
# ]
|
|
# [tool.ty.analysis]
|
|
# allowed-unresolved-imports = ["cadquery", "OCP.**", "generators.**"]
|
|
# ///
|
|
from __future__ import annotations
|
|
|
|
from argparse import ArgumentParser
|
|
from dataclasses import dataclass, replace
|
|
from math import cos, radians, sin
|
|
from pathlib import Path
|
|
import re
|
|
import subprocess
|
|
|
|
import cadquery as cq
|
|
from OCP.IFSelect import IFSelect_ReturnStatus
|
|
from OCP.Quantity import Quantity_Color, Quantity_TypeOfColor
|
|
from OCP.STEPCAFControl import STEPCAFControl_Writer
|
|
from OCP.TCollection import TCollection_ExtendedString
|
|
from OCP.TDocStd import TDocStd_Document
|
|
from OCP.TopLoc import TopLoc_Location
|
|
from OCP.XCAFApp import XCAFApp_Application
|
|
from OCP.XCAFDoc import XCAFDoc_ColorType, XCAFDoc_DocumentTool
|
|
from OCP.gp import gp_Trsf, gp_Vec
|
|
from generators.connector.pin_socket.cq_base_parameters import CaseType, PinStyle
|
|
from generators.connector.pin_socket.cq_socket_strips import (
|
|
angled_socket_strip,
|
|
smd_socket_strip,
|
|
socket_strip,
|
|
)
|
|
from generators.tools.model.stepreduce import stepreduce
|
|
|
|
|
|
DEFAULT_FOOTPRINT_ROOT = Path("lib/std/kicad-footprints")
|
|
DEFAULT_OUT_DIR = Path("target/kicad-step-gen/pinsockets")
|
|
DEFAULT_PCBC = Path("target/debug/pcbc")
|
|
|
|
FOOTPRINT_RE = re.compile(
|
|
r"PinSocket_(?P<rows>[12])x(?P<pins>\d{2})_P(?P<pitch>\d\.\d{2})mm_"
|
|
r"(?P<mount>Horizontal|Vertical(?:_SMD(?:_Pin1Left|_Pin1Right)?)?)"
|
|
r"\.kicad_mod$"
|
|
)
|
|
|
|
BLACK_RGB = (0.148000001907, 0.144999995828, 0.144999995828)
|
|
METAL_RGB = (0.85900002718, 0.737999975681, 0.495999991894)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class SocketSpec:
|
|
rows: int
|
|
pins: int
|
|
pitch: float
|
|
kind: str
|
|
model_class: str
|
|
params: dict[str, object]
|
|
|
|
@property
|
|
def name(self) -> str:
|
|
return f"PinSocket_{self.rows}x{self.pins:02}_P{self.pitch:.2f}mm_{self.kind}"
|
|
|
|
|
|
def tht_params(
|
|
*,
|
|
model_class: str,
|
|
rows: int,
|
|
pitch: float,
|
|
pin_style: str,
|
|
body_width: float,
|
|
body_height: float,
|
|
body_overlength: float,
|
|
body_offset: float,
|
|
pin_width: float,
|
|
pin_thickness: float,
|
|
pin_length: float,
|
|
pins_offset: float,
|
|
) -> dict[str, object]:
|
|
return {
|
|
"model_class": model_class,
|
|
"type": CaseType.THT,
|
|
"pin_style": pin_style,
|
|
"num_pin_rows": rows,
|
|
"pin_pitch": pitch,
|
|
"body_width": body_width,
|
|
"body_height": body_height,
|
|
"body_overlength": body_overlength,
|
|
"body_offset": body_offset,
|
|
"pin_width": pin_width,
|
|
"pin_thickness": pin_thickness,
|
|
"pin_length": pin_length,
|
|
"pins_offset": pins_offset,
|
|
"rotation": 90,
|
|
}
|
|
|
|
|
|
def smd_params(
|
|
*,
|
|
model_class: str,
|
|
rows: int,
|
|
pitch: float,
|
|
body_width: float,
|
|
body_height: float,
|
|
body_board_distance: float,
|
|
body_overlength: float,
|
|
pin1start_right: bool,
|
|
pin_width: float,
|
|
pin_thickness: float,
|
|
pin_length: float,
|
|
rotation: int,
|
|
) -> dict[str, object]:
|
|
return {
|
|
"model_class": model_class,
|
|
"type": CaseType.SMD,
|
|
"pin_style": PinStyle.STRAIGHT,
|
|
"num_pin_rows": rows,
|
|
"pin_pitch": pitch,
|
|
"body_width": body_width,
|
|
"body_height": body_height,
|
|
"body_board_distance": body_board_distance,
|
|
"body_overlength": body_overlength,
|
|
"body_offset": 0.0,
|
|
"pin1start_right": pin1start_right,
|
|
"pin_width": pin_width,
|
|
"pin_thickness": pin_thickness,
|
|
"pin_length": pin_length,
|
|
"pins_offset": 0.0,
|
|
"rotation": rotation,
|
|
}
|
|
|
|
|
|
SOCKET_VARIANTS: dict[tuple[str, int, float, str | None], dict[str, object]] = {
|
|
("Vertical", 1, 1.27, None): tht_params(
|
|
model_class="THT-1x1.27mm_Vertical",
|
|
rows=1,
|
|
pitch=1.27,
|
|
pin_style=PinStyle.STRAIGHT,
|
|
body_width=2.54,
|
|
body_height=4.4,
|
|
body_overlength=0.0,
|
|
body_offset=0.0,
|
|
pin_width=0.5,
|
|
pin_thickness=0.2,
|
|
pin_length=2.4,
|
|
pins_offset=0.0,
|
|
),
|
|
("Vertical", 2, 1.27, None): tht_params(
|
|
model_class="THT-2x1.27mm_Vertical",
|
|
rows=2,
|
|
pitch=1.27,
|
|
pin_style=PinStyle.STRAIGHT,
|
|
body_width=3.05,
|
|
body_height=4.4,
|
|
body_overlength=0.0,
|
|
body_offset=0.0,
|
|
pin_width=0.5,
|
|
pin_thickness=0.2,
|
|
pin_length=2.4,
|
|
pins_offset=0.0,
|
|
),
|
|
("Horizontal", 2, 1.27, None): tht_params(
|
|
model_class="THT-2x1.27mm_Horizontal",
|
|
rows=2,
|
|
pitch=1.27,
|
|
pin_style=PinStyle.ANGLED,
|
|
body_width=4.4,
|
|
body_height=3.10,
|
|
body_overlength=0.35,
|
|
body_offset=0.15,
|
|
pin_width=0.5,
|
|
pin_thickness=0.15,
|
|
pin_length=2.0,
|
|
pins_offset=0.0,
|
|
),
|
|
("Vertical_SMD", 1, 1.27, "Pin1Left"): smd_params(
|
|
model_class="SMD-1x1.27mm_Vertical_Left",
|
|
rows=1,
|
|
pitch=1.27,
|
|
body_width=1.8,
|
|
body_height=4.3,
|
|
body_board_distance=0.3,
|
|
body_overlength=0.35,
|
|
pin1start_right=False,
|
|
pin_width=0.42,
|
|
pin_thickness=0.15,
|
|
pin_length=3.5,
|
|
rotation=90,
|
|
),
|
|
("Vertical_SMD", 1, 1.27, "Pin1Right"): smd_params(
|
|
model_class="SMD-1x1.27mm_Vertical_Right",
|
|
rows=1,
|
|
pitch=1.27,
|
|
body_width=1.8,
|
|
body_height=4.3,
|
|
body_board_distance=0.3,
|
|
body_overlength=0.35,
|
|
pin1start_right=True,
|
|
pin_width=0.42,
|
|
pin_thickness=0.15,
|
|
pin_length=3.5,
|
|
rotation=-90,
|
|
),
|
|
("Vertical_SMD", 2, 1.27, None): smd_params(
|
|
model_class="SMD-2x1.27mm_Vertical",
|
|
rows=2,
|
|
pitch=1.27,
|
|
body_width=2.54,
|
|
body_height=4.4,
|
|
body_board_distance=0.2,
|
|
body_overlength=0.0,
|
|
pin1start_right=True,
|
|
pin_width=0.4,
|
|
pin_thickness=0.15,
|
|
pin_length=5.11,
|
|
rotation=90,
|
|
),
|
|
("Vertical", 1, 2.54, None): tht_params(
|
|
model_class="THT-1x2.54mm_Vertical",
|
|
rows=1,
|
|
pitch=2.54,
|
|
pin_style=PinStyle.STRAIGHT,
|
|
body_width=2.54,
|
|
body_height=8.5,
|
|
body_overlength=0.0,
|
|
body_offset=0.0,
|
|
pin_width=0.6,
|
|
pin_thickness=0.2,
|
|
pin_length=2.9,
|
|
pins_offset=0.0,
|
|
),
|
|
("Vertical", 2, 2.54, None): tht_params(
|
|
model_class="THT-2x2.54mm_Vertical",
|
|
rows=2,
|
|
pitch=2.54,
|
|
pin_style=PinStyle.STRAIGHT,
|
|
body_width=5.08,
|
|
body_height=8.5,
|
|
body_overlength=0.0,
|
|
body_offset=0.0,
|
|
pin_width=0.6,
|
|
pin_thickness=0.2,
|
|
pin_length=2.9,
|
|
pins_offset=0.0,
|
|
),
|
|
("Horizontal", 1, 2.54, None): tht_params(
|
|
model_class="THT-1x2.54mm_Horizontal",
|
|
rows=1,
|
|
pitch=2.54,
|
|
pin_style=PinStyle.ANGLED,
|
|
body_width=8.51,
|
|
body_height=2.54,
|
|
body_overlength=0.0,
|
|
body_offset=1.52,
|
|
pin_width=0.6,
|
|
pin_thickness=0.2,
|
|
pin_length=2.9,
|
|
pins_offset=0.0,
|
|
),
|
|
("Horizontal", 2, 2.54, None): tht_params(
|
|
model_class="THT-2x2.54mm_Horizontal",
|
|
rows=2,
|
|
pitch=2.54,
|
|
pin_style=PinStyle.ANGLED,
|
|
body_width=8.51,
|
|
body_height=5.08,
|
|
body_overlength=0.0,
|
|
body_offset=1.52,
|
|
pin_width=0.6,
|
|
pin_thickness=0.2,
|
|
pin_length=2.5,
|
|
pins_offset=0.0,
|
|
),
|
|
("Vertical_SMD", 1, 2.54, "Pin1Left"): smd_params(
|
|
model_class="SMD-1x2.54mm_Vertical_Left",
|
|
rows=1,
|
|
pitch=2.54,
|
|
body_width=2.54,
|
|
body_height=7.1,
|
|
body_board_distance=0.4,
|
|
body_overlength=0.2,
|
|
pin1start_right=False,
|
|
pin_width=0.6,
|
|
pin_thickness=0.2,
|
|
pin_length=4.54,
|
|
rotation=90,
|
|
),
|
|
("Vertical_SMD", 1, 2.54, "Pin1Right"): smd_params(
|
|
model_class="SMD-1x2.54mm_Vertical_Right",
|
|
rows=1,
|
|
pitch=2.54,
|
|
body_width=2.54,
|
|
body_height=7.1,
|
|
body_board_distance=0.4,
|
|
body_overlength=0.2,
|
|
pin1start_right=True,
|
|
pin_width=0.6,
|
|
pin_thickness=0.2,
|
|
pin_length=4.54,
|
|
rotation=-90,
|
|
),
|
|
("Vertical_SMD", 2, 2.54, None): smd_params(
|
|
model_class="SMD-2x2.54mm_Vertical",
|
|
rows=2,
|
|
pitch=2.54,
|
|
body_width=5.08,
|
|
body_height=7.15,
|
|
body_board_distance=0.3,
|
|
body_overlength=0.0,
|
|
pin1start_right=True,
|
|
pin_width=0.64,
|
|
pin_thickness=0.2,
|
|
pin_length=6.70,
|
|
rotation=90,
|
|
),
|
|
}
|
|
|
|
|
|
def qcolor(rgb: tuple[float, float, float]) -> Quantity_Color:
|
|
return Quantity_Color(*rgb, Quantity_TypeOfColor.Quantity_TOC_sRGB)
|
|
|
|
|
|
def loc(delta: tuple[float, float, float]) -> TopLoc_Location:
|
|
trsf = gp_Trsf()
|
|
trsf.SetTranslation(gp_Vec(*delta))
|
|
return TopLoc_Location(trsf)
|
|
|
|
|
|
def shape(workplane: cq.Workplane):
|
|
return workplane.val().wrapped
|
|
|
|
|
|
def param_float(params: dict[str, object], key: str) -> float:
|
|
value = params[key]
|
|
if not isinstance(value, int | float):
|
|
raise TypeError(f"{key} must be numeric")
|
|
return float(value)
|
|
|
|
|
|
def param_int(params: dict[str, object], key: str) -> int:
|
|
value = params[key]
|
|
if not isinstance(value, int):
|
|
raise TypeError(f"{key} must be an integer")
|
|
return value
|
|
|
|
|
|
def make_doc(name: str) -> TDocStd_Document:
|
|
app = XCAFApp_Application.GetApplication_s()
|
|
doc = TDocStd_Document(TCollection_ExtendedString(name))
|
|
app.NewDocument(TCollection_ExtendedString("MDTV-XCAF"), doc)
|
|
return doc
|
|
|
|
|
|
def rotate_z(
|
|
delta: tuple[float, float, float], degrees: float
|
|
) -> tuple[float, float, float]:
|
|
angle = radians(degrees)
|
|
x, y, z = delta
|
|
return (
|
|
x * cos(angle) - y * sin(angle),
|
|
x * sin(angle) + y * cos(angle),
|
|
z,
|
|
)
|
|
|
|
|
|
def parse_footprint(path: Path) -> SocketSpec:
|
|
match = FOOTPRINT_RE.fullmatch(path.name)
|
|
if match is None:
|
|
raise ValueError(f"not a supported pin socket footprint: {path}")
|
|
|
|
rows = int(match["rows"])
|
|
pins = int(match["pins"])
|
|
pitch = float(match["pitch"])
|
|
mount = match["mount"]
|
|
if mount.startswith("Vertical_SMD"):
|
|
kind = "Vertical_SMD"
|
|
pin_1_start = (
|
|
"Pin1Right"
|
|
if mount.endswith("_Pin1Right")
|
|
else ("Pin1Left" if rows == 1 else None)
|
|
)
|
|
else:
|
|
kind = mount
|
|
pin_1_start = None
|
|
|
|
params = SOCKET_VARIANTS[(kind, rows, pitch, pin_1_start)]
|
|
return SocketSpec(
|
|
rows=rows,
|
|
pins=pins,
|
|
pitch=pitch,
|
|
kind=mount,
|
|
model_class=str(params["model_class"]),
|
|
params=params,
|
|
)
|
|
|
|
|
|
def discover_footprints(root: Path) -> list[tuple[Path, SocketSpec]]:
|
|
footprints = sorted(root.glob("Connector_PinSocket_*.pretty/PinSocket_*.kicad_mod"))
|
|
return [(path, parse_footprint(path)) for path in footprints]
|
|
|
|
|
|
def export_translation(spec: SocketSpec) -> tuple[float, float, float]:
|
|
p = spec.params
|
|
pin_num = spec.pins
|
|
pitch = param_float(p, "pin_pitch")
|
|
rows = param_int(p, "num_pin_rows")
|
|
pin_width = param_float(p, "pin_width")
|
|
pin_thickness = param_float(p, "pin_thickness")
|
|
model_class = spec.model_class
|
|
|
|
if model_class == "THT-1x1.27mm_Vertical":
|
|
return (
|
|
pitch * (pin_num / 2.0 / rows - 0.5),
|
|
pin_width / 2.0 - pin_thickness - 0.05,
|
|
0.0,
|
|
)
|
|
if model_class == "THT-1x2.54mm_Vertical":
|
|
return (
|
|
pitch * (pin_num / 2.0 / rows - 0.5),
|
|
pin_width / 2.0 - pin_thickness - 0.1,
|
|
0.0,
|
|
)
|
|
if model_class.startswith("THT-1") and model_class.endswith("Horizontal"):
|
|
return (pitch * (pin_num - 1) / 2.0, 0.0, 0.0)
|
|
if model_class.startswith("THT-2") and model_class.endswith("Horizontal"):
|
|
return (
|
|
pitch * (pin_num - 1) / 2.0,
|
|
pitch / 2.0 - pin_width / 2.0 + pin_thickness + 0.1,
|
|
0.0,
|
|
)
|
|
if model_class.startswith("SMD"):
|
|
return (0.0, 0.0, pin_width / 2.0)
|
|
return (pitch * (pin_num - 1) / 2.0, pitch / 2.0, 0.0)
|
|
|
|
|
|
def export_transform(spec: SocketSpec, part: cq.Workplane) -> cq.Workplane:
|
|
tx, ty, tz = export_translation(spec)
|
|
part = part.translate((-tx, ty, tz))
|
|
rotation = param_float(spec.params, "rotation")
|
|
if rotation:
|
|
part = part.rotate((0, 0, 0), (0, 0, 1), rotation)
|
|
return part
|
|
|
|
|
|
def cqm_for_columns(
|
|
spec: SocketSpec, columns: int, *, body_overlength: float | None = None
|
|
):
|
|
params = dict(spec.params)
|
|
params["num_pins"] = spec.rows * columns
|
|
if body_overlength is not None:
|
|
params["body_overlength"] = body_overlength
|
|
if spec.model_class.startswith("SMD"):
|
|
return smd_socket_strip(params)
|
|
if spec.model_class.endswith("Horizontal"):
|
|
return angled_socket_strip(params)
|
|
return socket_strip(params)
|
|
|
|
|
|
def cqm_for(spec: SocketSpec):
|
|
return cqm_for_columns(spec, spec.pins)
|
|
|
|
|
|
def vertical_pin_components(
|
|
cqm,
|
|
) -> list[tuple[cq.Workplane, list[tuple[float, float, float]]]]:
|
|
pitch = cqm.pin_pitch
|
|
count = int(cqm.num_pins / cqm.num_pin_rows)
|
|
first = (
|
|
cqm._make_straight_pin()
|
|
.union(cqm._make_pinsocket())
|
|
.translate(cqm.first_pin_pos + (cqm.body_board_distance,))
|
|
)
|
|
components = [(first, [(-pitch * i, 0.0, 0.0) for i in range(count)])]
|
|
if cqm.num_pin_rows == 2:
|
|
second = first.rotate((0, 0, 0), (0, 0, 1), 180)
|
|
components.append((second, [(pitch * i, 0.0, 0.0) for i in range(count)]))
|
|
return components
|
|
|
|
|
|
def horizontal_pin_components(
|
|
cqm,
|
|
) -> list[tuple[cq.Workplane, list[tuple[float, float, float]]]]:
|
|
pitch = cqm.pin_pitch
|
|
count = int(cqm.num_pins / cqm.num_pin_rows)
|
|
tl = cqm.translate[1] - (0.0 if cqm.num_pin_rows == 1 else cqm.pin_pitch / 2.0)
|
|
|
|
first = (
|
|
cqm._make_angled_pin(
|
|
style=CaseType.THT,
|
|
pin_height=cqm.pin_length + cqm.pin_pitch / 2.0,
|
|
top_length=tl,
|
|
)
|
|
.union(cqm._make_pinsocket())
|
|
.translate(cqm.first_pin_pos + (cqm.body_board_distance,))
|
|
.rotate((0, 0, 0), (1, 0, 0), -90)
|
|
.translate(cqm.translate)
|
|
)
|
|
components = [(first, [(-pitch * i, 0.0, 0.0) for i in range(count)])]
|
|
|
|
if cqm.num_pin_rows == 2:
|
|
second = (
|
|
cqm._make_angled_pin(
|
|
style=CaseType.THT,
|
|
pin_height=cqm.pin_length + cqm.pin_pitch * 1.5,
|
|
top_length=tl + cqm.pin_pitch,
|
|
)
|
|
.union(cqm._make_pinsocket())
|
|
.translate(
|
|
(cqm.first_pin_pos[0], -cqm.first_pin_pos[1], cqm.body_board_distance)
|
|
)
|
|
.rotate((0, 0, 0), (1, 0, 0), -90)
|
|
.translate(cqm.translate)
|
|
)
|
|
components.append((second, [(-pitch * i, 0.0, 0.0) for i in range(count)]))
|
|
|
|
return components
|
|
|
|
|
|
def smd_pin_components(
|
|
cqm,
|
|
) -> list[tuple[cq.Workplane, list[tuple[float, float, float]]]]:
|
|
pitch = cqm.pin_pitch
|
|
base = (
|
|
cqm._make_angled_pin(top_length=cqm.body_board_distance)
|
|
.union(cqm._make_pinsocket())
|
|
.translate((0.0, 0.0, cqm.body_board_distance))
|
|
)
|
|
|
|
if cqm.num_pin_rows == 1:
|
|
first = base.translate(
|
|
(
|
|
cqm.first_pin_pos[0] + (0.0 if not cqm.pin1start_right else -pitch),
|
|
cqm.first_pin_pos[1],
|
|
0.0,
|
|
)
|
|
)
|
|
count = int(cqm.num_pins / 2)
|
|
components = [(first, [(-pitch * 2.0 * i, 0.0, 0.0) for i in range(count)])]
|
|
|
|
second = first.rotate((0, 0, 0), (0, 0, 1), 180)
|
|
second_offsets = [(pitch * 2.0 * i, 0.0, 0.0) for i in range(count)]
|
|
if cqm.odd_pins:
|
|
if cqm.pin1start_right:
|
|
odd_pin = first.rotate((0, 0, 0), (0, 0, 1), 180).translate(
|
|
(-pitch, 0.0, 0.0)
|
|
)
|
|
else:
|
|
odd_pin = first.translate((-cqm.first_pin_pos[0] * 2.0, 0.0, 0.0))
|
|
components.append((odd_pin, [(0.0, 0.0, 0.0)]))
|
|
second_offsets = [(x + pitch, y, z) for x, y, z in second_offsets]
|
|
components.append((second, second_offsets))
|
|
return components
|
|
|
|
first = base.translate(cqm.first_pin_pos + (0.0,))
|
|
count = int(cqm.num_pins / cqm.num_pin_rows)
|
|
components = [(first, [(-pitch * i, 0.0, 0.0) for i in range(count)])]
|
|
second = first.rotate((0, 0, 0), (0, 0, 1), 180)
|
|
components.append((second, [(pitch * i, 0.0, 0.0) for i in range(count)]))
|
|
return components
|
|
|
|
|
|
def pin_components(
|
|
spec: SocketSpec, cqm
|
|
) -> list[tuple[cq.Workplane, list[tuple[float, float, float]]]]:
|
|
if spec.model_class.startswith("SMD"):
|
|
return smd_pin_components(cqm)
|
|
if spec.model_class.endswith("Horizontal"):
|
|
return horizontal_pin_components(cqm)
|
|
return vertical_pin_components(cqm)
|
|
|
|
|
|
def body_cell_offsets(spec: SocketSpec) -> list[tuple[float, float, float]]:
|
|
if spec.model_class.startswith("SMD"):
|
|
return [
|
|
(((spec.pins - 1) / 2.0 - i) * spec.pitch, 0.0, 0.0)
|
|
for i in range(spec.pins)
|
|
]
|
|
return [(-spec.pitch * i, 0.0, 0.0) for i in range(spec.pins)]
|
|
|
|
|
|
def make_body_end_cap(spec: SocketSpec, cqm, length: float) -> cq.Workplane:
|
|
width = cqm.body_width
|
|
body = cq.Workplane(cq.Plane.XY()).rect(length, width).extrude(cqm.body_height)
|
|
if cqm.body_board_distance > 0.0:
|
|
body = (
|
|
body.faces("<Z")
|
|
.rect(length, width - width / 3.0)
|
|
.cutBlind(cqm.body_board_distance)
|
|
)
|
|
if spec.model_class.endswith("Horizontal"):
|
|
body = body.rotate((0, 0, 0), (1, 0, 0), -90).translate(cqm.translate)
|
|
return body
|
|
|
|
|
|
def body_end_cap_offsets(
|
|
spec: SocketSpec, cap_length: float
|
|
) -> list[tuple[float, float, float]]:
|
|
if cap_length <= 0.0:
|
|
return []
|
|
|
|
if spec.model_class.startswith("SMD"):
|
|
center = spec.pins * spec.pitch / 2.0 + cap_length / 2.0
|
|
return [(-center, 0.0, 0.0), (center, 0.0, 0.0)]
|
|
|
|
return [
|
|
(-(spec.pins - 1) * spec.pitch - cap_length / 2.0, 0.0, 0.0),
|
|
(cap_length / 2.0, 0.0, 0.0),
|
|
]
|
|
|
|
|
|
def add_body_components(
|
|
shape_tool,
|
|
color_tool,
|
|
assembly,
|
|
spec: SocketSpec,
|
|
cqm,
|
|
) -> None:
|
|
rotation = param_float(spec.params, "rotation")
|
|
cell_spec = replace(spec, pins=1)
|
|
cell_cqm = cqm_for_columns(spec, 1, body_overlength=0.0)
|
|
|
|
body_label = shape_tool.AddShape(
|
|
shape(export_transform(cell_spec, cell_cqm._make_body())),
|
|
False,
|
|
)
|
|
color_tool.SetColor(
|
|
body_label,
|
|
qcolor(BLACK_RGB),
|
|
XCAFDoc_ColorType.XCAFDoc_ColorSurf,
|
|
)
|
|
for offset in body_cell_offsets(spec):
|
|
shape_tool.AddComponent(assembly, body_label, loc(rotate_z(offset, rotation)))
|
|
|
|
body_overlength = param_float(spec.params, "body_overlength")
|
|
cap_length = body_overlength / 2.0
|
|
if cap_length <= 0.0:
|
|
return
|
|
|
|
cap_spec = spec if spec.model_class.startswith("SMD") else cell_spec
|
|
cap_label = shape_tool.AddShape(
|
|
shape(export_transform(cap_spec, make_body_end_cap(spec, cqm, cap_length))),
|
|
False,
|
|
)
|
|
color_tool.SetColor(
|
|
cap_label,
|
|
qcolor(BLACK_RGB),
|
|
XCAFDoc_ColorType.XCAFDoc_ColorSurf,
|
|
)
|
|
for offset in body_end_cap_offsets(spec, cap_length):
|
|
shape_tool.AddComponent(assembly, cap_label, loc(rotate_z(offset, rotation)))
|
|
|
|
|
|
def write_step(path: Path, spec: SocketSpec, reduce: bool) -> None:
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
cqm = cqm_for(spec)
|
|
|
|
doc = make_doc(path.stem)
|
|
shape_tool = XCAFDoc_DocumentTool.ShapeTool_s(doc.Main())
|
|
color_tool = XCAFDoc_DocumentTool.ColorTool_s(doc.Main())
|
|
assembly = shape_tool.NewShape()
|
|
|
|
add_body_components(shape_tool, color_tool, assembly, spec, cqm)
|
|
|
|
rotation = param_float(spec.params, "rotation")
|
|
for pin, offsets in pin_components(spec, cqm):
|
|
pin_label = shape_tool.AddShape(shape(export_transform(spec, pin)), False)
|
|
color_tool.SetColor(
|
|
pin_label,
|
|
qcolor(METAL_RGB),
|
|
XCAFDoc_ColorType.XCAFDoc_ColorSurf,
|
|
)
|
|
for offset in offsets:
|
|
shape_tool.AddComponent(
|
|
assembly, pin_label, loc(rotate_z(offset, rotation))
|
|
)
|
|
|
|
shape_tool.UpdateAssemblies()
|
|
|
|
writer = STEPCAFControl_Writer()
|
|
writer.SetColorMode(True)
|
|
writer.SetNameMode(True)
|
|
if not writer.Transfer(assembly):
|
|
raise RuntimeError("STEP writer transfer failed")
|
|
status = writer.Write(str(path))
|
|
if status != IFSelect_ReturnStatus.IFSelect_RetDone:
|
|
raise RuntimeError(f"STEP writer failed: {status}")
|
|
|
|
if reduce:
|
|
stepreduce(str(path), str(path))
|
|
|
|
|
|
def build_parser() -> ArgumentParser:
|
|
parser = ArgumentParser(
|
|
description="Generate compact solid STEP assemblies for stdlib KiCad pin sockets.",
|
|
)
|
|
parser.add_argument("--footprint-root", type=Path, default=DEFAULT_FOOTPRINT_ROOT)
|
|
parser.add_argument("--out-dir", type=Path, default=DEFAULT_OUT_DIR)
|
|
parser.add_argument("--pcbc", type=Path, default=DEFAULT_PCBC)
|
|
parser.add_argument(
|
|
"--embed",
|
|
action="store_true",
|
|
help="Embed each generated STEP into its matching .kicad_mod using pcbc embed-step.",
|
|
)
|
|
parser.add_argument(
|
|
"--no-reduce",
|
|
action="store_true",
|
|
help="Skip KiCad stepreduce post-processing.",
|
|
)
|
|
parser.add_argument(
|
|
"--filter",
|
|
help="Only process footprints whose filename contains this string.",
|
|
)
|
|
return parser
|
|
|
|
|
|
def embed_step(pcbc: Path, footprint: Path, step: Path) -> None:
|
|
subprocess.run(
|
|
[str(pcbc), "embed-step", str(footprint), str(step)],
|
|
check=True,
|
|
)
|
|
|
|
|
|
def main() -> None:
|
|
args = build_parser().parse_args()
|
|
footprints = discover_footprints(args.footprint_root)
|
|
if args.filter:
|
|
footprints = [item for item in footprints if args.filter in item[0].name]
|
|
|
|
if args.embed and not args.pcbc.exists():
|
|
raise SystemExit(f"{args.pcbc} does not exist; run `cargo build -p pcbc` first")
|
|
|
|
total = len(footprints)
|
|
if total == 0:
|
|
raise SystemExit("no pin socket footprints matched")
|
|
|
|
generated_bytes = 0
|
|
for index, (footprint, spec) in enumerate(footprints, start=1):
|
|
step = args.out_dir / f"{spec.name}.step"
|
|
write_step(step, spec, reduce=not args.no_reduce)
|
|
generated_bytes += step.stat().st_size
|
|
if args.embed:
|
|
embed_step(args.pcbc, footprint, step)
|
|
if index % 25 == 0 or index == total:
|
|
action = "generated and embedded" if args.embed else "generated"
|
|
print(f"{action} {index}/{total}")
|
|
|
|
print(f"wrote {total} STEP files ({generated_bytes:,} bytes)")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|