by adom
0603 SMD Green LED · 3.2 V drop · 25 mA max
master
John Lauer
LED colour pass: coloured STEP is the default (lens colour from the datasheet dominant wavelength, softened), plain STE…
c7192f0
20d ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
#!/usr/bin/env python3
"""Colour an LED STEP's lens (and give body/terminals colours where they have none) with XCAF.
Geometry, topology, product names and the file's leading comment block are kept; only face colours
change. Every coloured face gets BOTH XCAFDoc_ColorSurf and XCAFDoc_ColorGen. Colours are given in
sRGB (as written to COLOUR_RGB in the file) and passed to OCCT as Quantity_TOC_sRGB.
Families (classification is by the source model's own colours + geometry, never by guesswork):
kicad-chip KiCad LED_0402_1005Metric / LED_0603_1608Metric: one solid; ivory faces above the
substrate top form the moulded lens block (the clear lens); ivory faces at/below it are
the PCB substrate; gold = terminals; green = underside cathode mark.
template a colourless copy of a kicad-chip model with the same face order (IN-S42ATR is the KiCad
0402 model scaled to 0.45 mm in Z and re-exported as B-splines): classes are copied from
--template by face index after checking every face's X/Y extent matches.
ws5050 KiCad LED_WS2812B_PLCC4_5.0x5.0mm: ivory = body, blue disc = cavity/die window, grey = leads.
Usage: colour_led.py SRC OUT --family F --lens R,G,B [--body R,G,B] [--term R,G,B] [--mark R,G,B]
[--template TEMPLATE.step] [--report report.json]"""
import sys, json, argparse, re
from OCP.STEPCAFControl import STEPCAFControl_Reader, STEPCAFControl_Writer
from OCP.STEPControl import STEPControl_AsIs
from OCP.TDocStd import TDocStd_Document
from OCP.TCollection import TCollection_ExtendedString
from OCP.XCAFDoc import XCAFDoc_DocumentTool, XCAFDoc_ColorType
from OCP.TDF import TDF_ChildIterator, TDF_Label
from OCP.Quantity import Quantity_Color, Quantity_TOC_sRGB
from OCP.Bnd import Bnd_Box
from OCP.BRepBndLib import BRepBndLib
from OCP.TopExp import TopExp_Explorer
from OCP.TopAbs import TopAbs_FACE
SURF, GEN = XCAFDoc_ColorType.XCAFDoc_ColorSurf, XCAFDoc_ColorType.XCAFDoc_ColorGen
KICAD_IVORY, KICAD_GOLD, KICAD_GREEN = (0.895, 0.891, 0.813), (0.859, 0.738, 0.496), (0.34, 0.68, 0.445)
def load(path):
doc = TDocStd_Document(TCollection_ExtendedString("d"))
r = STEPCAFControl_Reader(); r.SetColorMode(True); r.SetNameMode(True); r.SetLayerMode(True)
assert r.ReadFile(path) == 1, path
r.Transfer(doc)
st = XCAFDoc_DocumentTool.ShapeTool_s(doc.Main()); ct = XCAFDoc_DocumentTool.ColorTool_s(doc.Main())
free = []
it = TDF_ChildIterator(st.Label(), False)
while it.More():
l = it.Value(); it.Next()
if st.IsFree_s(l) and st.IsTopLevel(l): free.append(l)
assert len(free) == 1 and not st.IsAssembly_s(free[0]), 'expected one simple part'
return doc, st, ct, free[0]
def faces(st, ct, lab):
out = []; ex = TopExp_Explorer(st.GetShape_s(lab), TopAbs_FACE)
while ex.More():
f = ex.Current(); ex.Next()
b = Bnd_Box(); BRepBndLib.Add_s(f, b); mn, mx = b.CornerMin(), b.CornerMax()
c = Quantity_Color(); col = None
for t in (SURF, GEN):
if ct.GetColor(f, t, c):
r, g, bb = c.Values(Quantity_TOC_sRGB); col = (round(r, 3), round(g, 3), round(bb, 3)); break
out.append(dict(face=f, col=col, mn=(mn.X(), mn.Y(), mn.Z()), mx=(mx.X(), mx.Y(), mx.Z())))
return out
def near(a, b, tol=0.02): return a is not None and all(abs(x - y) < tol for x, y in zip(a, b))
def classify_kicad_chip(fs):
ivory = [f for f in fs if near(f['col'], KICAD_IVORY)]
ztop = max(f['mx'][2] for f in fs)
# substrate top = the z of the small horizontal ivory faces beside the lens block
zsub = min(f['mn'][2] for f in ivory if f['mn'][2] > 0.05 * ztop)
cls = []
for f in fs:
if near(f['col'], KICAD_IVORY):
lens = f['mn'][2] >= zsub - 1e-4 and f['mx'][2] > zsub + 0.02 * ztop
cls.append('lens' if lens else 'substrate')
elif near(f['col'], KICAD_GOLD): cls.append('terminal')
elif near(f['col'], KICAD_GREEN): cls.append('cathode_mark')
else: raise SystemExit(f'unexpected colour {f["col"]}')
return cls, dict(z_top_mm=round(ztop, 4), z_substrate_top_mm=round(zsub, 4))
def classify_ws5050(fs):
cls = []
for f in fs:
if near(f['col'], KICAD_IVORY): cls.append('body')
elif near(f['col'], (0.15, 0.4, 0.73), 0.08) or near(f['col'], (0.02, 0.13, 0.49), 0.05): cls.append('window')
else: cls.append('terminal')
return cls, {}
def main():
ap = argparse.ArgumentParser(); ap.add_argument('src'); ap.add_argument('out')
ap.add_argument('--family', required=True, choices=['kicad-chip', 'template', 'ws5050'])
ap.add_argument('--template'); ap.add_argument('--report')
for k in ('lens', 'body', 'term', 'mark'): ap.add_argument('--' + k)
a = ap.parse_args()
rgb = lambda s: tuple(float(v) for v in s.split(',')) if s else None
doc, st, ct, lab = load(a.src); fs = faces(st, ct, lab)
info = {}
if a.family == 'kicad-chip': cls, info = classify_kicad_chip(fs)
elif a.family == 'ws5050': cls, info = classify_ws5050(fs)
else:
_, st2, ct2, lab2 = load(a.template); tf = faces(st2, ct2, lab2)
assert len(tf) == len(fs), (len(tf), len(fs))
for i, (f, t) in enumerate(zip(fs, tf)): # same face order: X/Y extents must agree
assert all(abs(f['mn'][k] - t['mn'][k]) < 2e-3 and abs(f['mx'][k] - t['mx'][k]) < 2e-3 for k in (0, 1)), i
cls, info = classify_kicad_chip(tf); info['template'] = a.template.split('/')[-1]
info['z_scale_vs_template'] = round(max(f['mx'][2] for f in fs) / max(f['mx'][2] for f in tf), 4)
colours = {'lens': rgb(a.lens)}
if a.family == 'ws5050':
colours.update(body=rgb(a.body), window=rgb(a.lens), terminal=rgb(a.term)); colours.pop('lens')
else:
colours.update(substrate=rgb(a.body) or KICAD_IVORY, terminal=rgb(a.term) or KICAD_GOLD,
cathode_mark=rgb(a.mark) or KICAD_GREEN)
counts = {}; changed = 0
for f, c in zip(fs, cls):
want = colours[c]; counts[c] = counts.get(c, 0) + 1
if want is None or (f['col'] is not None and near(f['col'], want, 1e-3)): continue
sub = TDF_Label()
if not st.FindSubShape(lab, f['face'], sub): sub = st.AddSubShape(lab, f['face'])
q = Quantity_Color(want[0], want[1], want[2], Quantity_TOC_sRGB)
ct.SetColor(sub, q, SURF); ct.SetColor(sub, q, GEN); changed += 1
w = STEPCAFControl_Writer(); w.SetColorMode(True); w.SetNameMode(True); w.SetLayerMode(True)
w.Transfer(doc, STEPControl_AsIs); assert w.Write(a.out) == 1
# keep the source's leading /* ... */ comment block (KiCad licence/attribution) in the HEADER
src_txt = open(a.src, encoding='latin-1').read(); m = re.search(r'HEADER;\s*(/\*.*?\*/)', src_txt, re.S)
if m:
t = open(a.out, encoding='latin-1').read().replace('HEADER;', 'HEADER;\n' + m.group(1), 1)
open(a.out, 'w', encoding='latin-1').write(t)
rep = dict(src=a.src.split('/')[-1], out=a.out.split('/')[-1], family=a.family, faces=len(fs),
faces_by_class=counts, faces_recoloured=changed,
colours_srgb={k: v for k, v in colours.items()}, classification=info, kept_comment_block=bool(m))
if a.report: json.dump(rep, open(a.report, 'w'), indent=1)
print(json.dumps(rep))
if __name__ == '__main__': main()