app
Adom Chip Thumbnailer
Public Made by Adomby adom
The chip-icon factory: from a STEP file it renders the complete family of chip icons, shaded 3D orientations, transparent icons, name-lasered variants, and 11 vector outline styles, in a live web app + CLI.
master
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
#!/usr/bin/env python3
"""Etch an MPN onto a part STEP the planner's way, on the shared service only (no OpenCascade):
1. plan (mark_plan.py): face, long axis, usable band, reading direction from pin 1, lines, size;
2. turn the part (exact quarter turns on the STEP text): stand it up, bring a side face to the top for
LEDs, and pose it so the reading direction is +X;
3. POST /laser-etch?mode=flat to service-step2glb on a thin proxy plate laid on the planned band, then
graft the real model (untouched, colours kept) under Adom.Chip next to the Adom.LaserEtch group;
4. fit the service's mark into the band: line 2 brought up to line 1's size, the block scaled to fill
the band minus margins, centred on it and seated on the face (coplanar, 5 um lift);
5. turn everything back, so the output sits in the input's own frame (board model paths and
transforms do not change), then run the legibility gate (mark_check.py) on the result."""
import json, os, sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import step_text as ST
import mark_svc as SVC
import mark_plan as MP
import mark_geom as MG
LIFT = 0.005
PROXY = os.path.join(os.path.dirname(os.path.abspath(__file__)), "proxy_box.step")
PROXY_T = 0.05
def _rect_turn(r, q):
pts = [MP._rot([x, y, 0], "z", q) for x, y in ((r[0], r[1]), (r[2], r[3]))]
return [min(p[0] for p in pts), min(p[1] for p in pts), max(p[0] for p in pts), max(p[1] for p in pts)]
def _set_mark_colour(step, rgb):
"""Recolour the mark (sRGB 0..1) on its styled items; the default is the service's light grey-white."""
ents = step.product_entities("Adom.LaserEtch") or set()
for k in step.order:
if step.kind(k) == "STYLED_ITEM" and set(step.refs(k)) & ents:
for x in step.reach([k]):
if step.kind(x) == "COLOUR_RGB":
nm = step.body[x].split(",", 1)[0]
step.body[x] = nm + "," + ",".join(ST.fmt(v) for v in rgb) + ")"
def etch(src, mpn, out, footprint=None, face="auto", up="auto", side_band="high", along=None, terminations="geometry",
margin_w=MP.MARGIN_W, margin_h=MP.MARGIN_H, lines=None, over_vendor_mark=False, keep_old_mark=False, mark=None, log=print):
plan = MP.plan(src, footprint, mpn, face, up, side_band, margin_w, margin_h, along=along, terminations=terminations)
if plan["vendorMarking"]["present"] and not over_vendor_mark:
raise SystemExit("ERROR: " + plan["warnings"][0] + "\nHint: leave this part's vendor model as it is; pass --over-vendor-mark only if the user asks for an Adom mark on top.")
text_lines = lines or plan["text"]["lines"]
s = ST.Step(src)
s.flatten()
removed = []
for nm in plan["existingMarks"]:
if not keep_old_mark:
s.remove_product(nm); removed.append(nm)
turns = [(t["axis"], t["quarterTurns"]) for t in plan["standUp"]]
st = plan["band"]["sideTurn"]
if st:
turns.append((st["axis"], st["quarterTurns"]))
qz = plan["etch"]["poseQuarterTurnsZ"]
if qz:
turns.append(("z", qz))
for ax, q in turns:
s.rotate90(ax, q)
band = _rect_turn(plan["band"]["regionMm"], qz)
bz = plan["band"]["zMm"]
# the service etches a thin PROXY plate laid exactly on the band (any model works, B-spline faces
# included, and the service can never pick a cap, a lens or a pin-1 dot); the real model is then
# grafted, untouched, under Adom.Chip
proxy = ST.Step(PROXY).scale_axes(band[0], band[1], bz - PROXY_T, band[2] - band[0], band[3] - band[1], PROXY_T)
etched = SVC.laser_etch(proxy.text, text_lines[0], text_lines[1] if len(text_lines) > 1 else None, job=f"etch-{mpn}")
e = ST.Step(etched)
e.flatten()
m = MG.analyse(e, "Adom.LaserEtch")
if not m:
raise SystemExit("ERROR: the service returned no Adom.LaserEtch group")
# 1) the service writes along its own idea of the long axis: turn the mark so it reads +X
q_mark = MP.quarter_turns_to(m["u"], (1, 0))
if q_mark:
e.transform_product("Adom.LaserEtch", m["centre"], 1.0, q_mark)
m = MG.analyse(e, "Adom.LaserEtch")
# 2) even out the lines: the service draws line 2 at about 0.62 of line 1
if len(m["lines"]) == 2:
l1, l2 = m["lines"]
k = l1["capMm"] / max(l2["capMm"], 1e-9)
ents2 = e.faces_subset_entities("Adom.LaserEtch", l2["faces"])
c2 = [l2["centre"][0], l2["centre"][1], m["z"]]
gap = MP.GAP * l1["capMm"]
new_top = l1["box"][1] - gap
new_h = (l2["box"][3] - l2["box"][1]) * k
dy = (new_top - new_h / 2) - c2[1]
dx = l1["centre"][0] - c2[0]
e.transform_entities(ents2, c2, k, (dx, dy, 0))
m = MG.analyse(e, "Adom.LaserEtch")
# 3) fill the band minus margins, centred, seated on the face
bw, bh = band[2] - band[0], band[3] - band[1]
mw_, mh_ = m["bbox"][3] - m["bbox"][0], m["bbox"][4] - m["bbox"][1]
scale = min(bw * (1 - 2 * margin_w) / mw_, bh * (1 - 2 * margin_h) / mh_)
c = m["centre"]
target = [(band[0] + band[2]) / 2, (band[1] + band[3]) / 2]
e.transform_product("Adom.LaserEtch", (c[0], c[1], c[2]), scale, 0, (target[0] - c[0], target[1] - c[1], 0), set_z=bz + LIFT)
fitted = MG.analyse(e, "Adom.LaserEtch")
e.graft(s, "Adom.Chip")
# 4) back to the input's frame
for ax, q in reversed(turns):
e.rotate90(ax, -q)
if mark not in (None, "light"):
rgb = {"dark": [0.11, 0.11, 0.11]}.get(mark) or [float(v) for v in mark.split(",")]
_set_mark_colour(e, rgb)
e.write(out)
info = dict(out=os.path.abspath(out), mpn=mpn, lines=text_lines, removedOldMarks=removed, turns=[dict(axis=a, quarterTurns=q) for a, q in turns],
serviceMarkTurnedQuarter=q_mark, line2Scaled=len(text_lines) == 2,
fit=dict(scale=round(scale, 4), capMm=round(fitted["capMm"], 4), markMm=[round(fitted["bbox"][3] - fitted["bbox"][0], 4), round(fitted["bbox"][4] - fitted["bbox"][1], 4)],
bandMm=[round(bw, 4), round(bh, 4)], zMm=round(bz + LIFT, 4)))
return plan, info
def main(argv=None):
import argparse
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--in", dest="src", required=True); ap.add_argument("--mpn", required=True); ap.add_argument("--out", required=True)
ap.add_argument("--footprint", help="the part's .kicad_mod: pin 1 / pad 1, pads and the model rotation (strongly recommended; a single .kicad_mod beside the STEP is used automatically)")
ap.add_argument("--face", default="auto", choices=["auto", "top", "side"])
ap.add_argument("--up", default="auto", choices=["auto", "z", "y"])
ap.add_argument("--side-band", default="high", choices=["high", "low"])
ap.add_argument("--along", choices=["long", "x", "y"], help="OVERRIDE the planner's axis (x/y in the stood-up frame); warns when it puts the text across the short side")
ap.add_argument("--terminations", default="geometry", choices=["geometry", "pads"])
ap.add_argument("--margin-w", type=float, default=MP.MARGIN_W); ap.add_argument("--margin-h", type=float, default=MP.MARGIN_H)
ap.add_argument("--lines", help="force the text lines, '|' separated (default: the planner's choice)")
ap.add_argument("--over-vendor-mark", action="store_true"); ap.add_argument("--keep-old-mark", action="store_true")
ap.add_argument("--mark", default="light", help="light (default, the rule) | dark (only on a light body) | r,g,b sRGB 0..1")
ap.add_argument("--frac", help=argparse.SUPPRESS); ap.add_argument("--side-dir", help=argparse.SUPPRESS); ap.add_argument("--service", help=argparse.SUPPRESS)
ap.add_argument("--no-check", action="store_true", help="skip the legibility gate (not recommended)")
ap.add_argument("--outline", help="also check this outline SVG against the mark")
a = ap.parse_args(argv)
fp = a.footprint
if not fp:
d = os.path.dirname(os.path.abspath(a.src))
c = [os.path.join(d, f) for f in os.listdir(d) if f.endswith(".kicad_mod")]
if len(c) == 1:
fp = c[0]; print(f"Hint: using footprint {fp} (the only .kicad_mod beside the STEP)", file=sys.stderr)
for flag in ("frac", "side_dir", "service"):
if getattr(a, flag):
print(f"Hint: --{flag.replace('_', '-')} is ignored now: the planner sizes the text to the band and picks the front wall from pin 1 (adom-aiflow-etch --legacy keeps the old OpenCascade path)", file=sys.stderr)
along = None if a.along in (None, "long") else a.along
plan, info = etch(a.src, a.mpn, a.out, fp, a.face, a.up, a.side_band, along, a.terminations, a.margin_w, a.margin_h,
a.lines.split("|") if a.lines else None, a.over_vendor_mark, a.keep_old_mark, a.mark)
for w in plan["warnings"]:
print("Hint: WARNING " + w, file=sys.stderr)
plan_path = os.path.splitext(a.out)[0] + ".mark-plan.json"
json.dump(dict(plan=plan, etch=info), open(plan_path, "w"), indent=1)
result = dict(ok=True, out=info["out"], plan=plan_path, summary=plan["summary"], etch=info)
rc = 0
if not a.no_check:
import mark_check as MC
rep = MC.check(a.out, plan=plan, footprint=fp, outline=a.outline, mpn=a.mpn)
result["check"] = dict(status=rep["status"], report=rep["reportPath"], image=rep.get("image"))
rc = 0 if rep["status"] != "FAIL" else 1
print(json.dumps(result))
return rc
if __name__ == "__main__":
sys.exit(main())