← Commit history
Cargo.lock+10−10
@@ -4,7 +4,7 @@ version = 4  [[package]] name = "adom-aiflow"-version = "0.1.43"+version = "0.1.44" dependencies = [  "aiflow-analyze",  "aiflow-board",@@ -24,7 +24,7 @@ dependencies = [  [[package]] name = "aiflow-analyze"-version = "0.1.43"+version = "0.1.44" dependencies = [  "serde",  "serde_json",@@ -33,7 +33,7 @@ dependencies = [  [[package]] name = "aiflow-board"-version = "0.1.43"+version = "0.1.44" dependencies = [  "serde",  "serde_json",@@ -41,7 +41,7 @@ dependencies = [  [[package]] name = "aiflow-bridge"-version = "0.1.43"+version = "0.1.44" dependencies = [  "aiflow-board",  "serde",@@ -50,7 +50,7 @@ dependencies = [  [[package]] name = "aiflow-copper"-version = "0.1.43"+version = "0.1.44" dependencies = [  "aiflow-board",  "aiflow-grid",@@ -60,7 +60,7 @@ dependencies = [  [[package]] name = "aiflow-grid"-version = "0.1.43"+version = "0.1.44" dependencies = [  "aiflow-board",  "serde",@@ -69,7 +69,7 @@ dependencies = [  [[package]] name = "aiflow-place"-version = "0.1.43"+version = "0.1.44" dependencies = [  "aiflow-board",  "serde",@@ -78,7 +78,7 @@ dependencies = [  [[package]] name = "aiflow-pours"-version = "0.1.43"+version = "0.1.44" dependencies = [  "aiflow-board",  "aiflow-copper",@@ -88,7 +88,7 @@ dependencies = [  [[package]] name = "aiflow-router"-version = "0.1.43"+version = "0.1.44" dependencies = [  "aiflow-board",  "aiflow-grid",@@ -98,7 +98,7 @@ dependencies = [  [[package]] name = "aiflow-run"-version = "0.1.43"+version = "0.1.44" dependencies = [  "serde",  "serde_json",
Cargo.toml+1−1
@@ -14,7 +14,7 @@ members = [ ]  [workspace.package]-version = "0.1.43"+version = "0.1.44" edition = "2021" license = "MIT" repository = "https://wiki.adom.inc/adom/adom-aiflow"
bin/adom-aiflow
⋯ 1 unchanged line ⋯
docs/release-0.1.44.mdadded+5
@@ -0,0 +1,5 @@+# AI Flow 0.1.44++- **The etch mark sits on the body, not on the end caps.** On MLCCs and chip resistors the ceramic top is 20 to 45 µm below the terminations, and the adom-chip-laser service lays its mark at the part's bounding-box top, so the text floated above the ceramic. `adom-aiflow-etch` now seats the mark on the surface actually under it (`tools/laser-etch/seat_mark.py`, vertical rays through the text footprint, 5 µm lift, colours kept).+- **`--along x|y`** turns the text off the long axis when a printed band is in the way: an electrolytic's text runs parallel to its polarity stripe, on the silver side.+- **Board flow rules for part marking:** keep a mark off polarity stripes, never etch over a vendor model that already prints its marking (Abracon's AMPLH5030S), use a light mark on dark and brown bodies, check that every marked ref's `(model ...)` points at the etched STEP (including parts on a KiCad stock model), and show the swap with `kicad-cli pcb render` before and after, because the desktop 3D viewer cannot be filmed while another window covers it.
flows/board.json+1−1
@@ -31,7 +31,7 @@         "MPN marking is ON by default for ICs (John, 2026-09-30): every IC's 3D model carries its MPN laser-etched on the top face, and finish checks it; passives are optional. Run `adom-aiflow-etch --in <model.step> --mpn <MPN> --out <model.step>` (adom/adom-chip-laser service plus a colour-preserving merge; the mark is flat, top faces only). It needs OpenCascade locally, the lightweight cadquery-ocp-novtk build (about 0.4 GB, no VTK), until the service keeps colours, turns the part and writes flat marks itself: ask the user before `AIFLOW_ALLOW_LOCAL_OCC=1`. Do not post a STEP to the service and use its output directly: the service repaints the whole part black and grey (adom/adom-chip-laser#2). Keep the plain model beside the marked one; `components --etch off` declines for a run. Marking is an identification aid, not the part's factory top mark.",         "Cache reviewed artifacts on the existing global component page: source/plain STEP, optional MPN STEP, GLB, thumbnails and machine-readable provenance. Record input hash, generator/tool version, parameters, units, transforms, marking text/mode, reference evidence and review results. Cache keys must change when geometry, text or generator parameters change. Keep board-only transforms and mappings in the board project. Never claim cross-EDA parity without rendering the variant in the named native viewers.",         "When publishing or improving a component page, make per-file provenance mandatory even though creating a new page is optional. Record original source URL/file and revision, retrieval date, source and output SHA-256, authoring classification (manufacturer-supplied, source-derived, AI-created, or unknown), generator/version and parameters, units/transforms, redistribution evidence and limitations. For AI-created geometry cite the actual datasheet page/figure/table and dimensions used, list simplifications and reference comparisons, and never present copied/extracted CAD as independent work. State which checks ran and which remain unverified; retain plain and marked variant lineage. Put a readable provenance section on the page plus a machine-readable asset record and a link to that component's issue tracker. Unknown provenance stays unknown, not a fabricated source. Reuse/improve existing pages first; offer new global-page publication only for reusable components, keeping board-specific records in the project.",-        "Fit the mark along the LONGEST usable top-face direction at the largest legible size (a SOT-23-6 gets 0.22 mm along its long axis against 0.14 mm across it); the service only writes across X, so turn the part first (adom-aiflow-etch does). Stand Y-up vendor models upright first (`--up y`), or the mark lands on a side face. Use a light mark on dark epoxy and a dark mark on light bodies (`--mark dark`). Look at the result: colours intact, text inside the top face, clear of pin-1 and polarity features; a mark across a polarity stripe or off the face is worse than none, so leave that part unmarked and say why.",+        "Fit the mark along the LONGEST usable top-face direction at the largest legible size (a SOT-23-6 gets 0.22 mm along its long axis against 0.14 mm across it); the service only writes across X, so turn the part first (adom-aiflow-etch does). Stand Y-up vendor models upright first (`--up y`), or the mark lands on a side face. Use a light mark on dark epoxy and a dark mark on light bodies (`--mark dark`). Look at the result: colours intact, text inside the top face, clear of pin-1 and polarity features; a mark across a polarity stripe is worse than none, so turn it onto the clear field with `--along x|y` (an electrolytic's text runs parallel to its stripe, on the silver side) and leave the part unmarked only when no field fits. If the vendor model already carries a printed marking (contrasting top-face colours spelling a part number, like Abracon's AMPLH5030S), keep the vendor model and do not etch over it. Swapping a model into the board: the board file must point at the etched STEP (check `(model ...)` for every marked ref, including parts that used a KiCad stock model), then show it with `kicad-cli pcb render` before and after, since the desktop 3D viewer cannot be filmed while another window covers it.",         "Offer the optional Hydrogen progress widget. If enabled, reuse saved component, marked-model, symbol and later board/analysis thumbnails via widget event; do not generate extra screenshots or call a model solely for the widget. Respect widget disable immediately.",         "Before accepting a wiki component, read its actual native model_3d and STEP bindings and hash the referenced downloads against the reviewed variants. Wait for the native viewer to finish painting, then inspect the visible body and readable mark; nonzero meshes or a successful HTTP request do not prove a painted model. Check the static hero too. Use a descriptive manufacturer/MPN/function title in both page.json and package.json; follow wiki-component and preserve other contributors and original files.",         "A resolved 3D file may still be hidden by native EDA appearance filters (for example KiCad excludes models not in position files). Check those filters before inventing replacement parts or changing BOM/placement flags. Keep bridge-specific visibility, binding and native audit operations in the owning bridge; report missing capabilities there. Explicit rights-holder permission may authorize a derivative despite a restrictive package default; record who authorized what and its scope, without extending it to third-party assets.",
package.json+1−1
@@ -1,7 +1,7 @@ {   "slug": "adom-aiflow",   "type": "app",-  "version": "0.1.43",+  "version": "0.1.44",   "title": "AI Flow",   "description": "Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board. One Rust binary with a crate per step (placement helpers, a grid router with Kelvin taps, pours with keepouts, KiCad's DRC gate, live landing through the KiCad Bridge, copper measurement, current and thermal analysis) and a finish line that refuses an unfinished board. Every command answers with hints for the AI; every turn, its thinking time and every rework loop go into run.jsonl, so Claude, Codex and any other engine are compared on the same flow. KiCad today; Altium, Fusion and Adom's own web apps next.",   "summary": "Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board. The AI thinks its way from placement through routing, pours, current and thermal analysis to a delivered video; the binary does the fast, deterministic parts of every step, hands back hints, and keeps a ledger of every turn, every return to an earlier step, and the clock from the prompt to done.",
page.json+1−1
@@ -1,7 +1,7 @@ {   "slug": "adom-aiflow",   "type": "app",-  "version": "0.1.43",+  "version": "0.1.44",   "title": "AI Flow",   "description": "Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board. One Rust binary with a crate per step (placement helpers, a grid router with Kelvin taps, pours with keepouts, KiCad's DRC gate, live landing through the KiCad Bridge, copper measurement, current and thermal analysis) and a finish line that refuses an unfinished board. Every command answers with hints for the AI; every turn, its thinking time and every rework loop go into run.jsonl, so Claude, Codex and any other engine are compared on the same flow. KiCad today; Altium, Fusion and Adom's own web apps next.",   "summary": "Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board. The AI thinks its way from placement through routing, pours, current and thermal analysis to a delivered video; the binary does the fast, deterministic parts of every step, hands back hints, and keeps a ledger of every turn, every return to an earlier step, and the clock from the prompt to done.",
skills/aiflow-sourcing/SKILL.md+1−1
@@ -109,7 +109,7 @@ step2glb thumbnail part.step       # look at it  ## Component wiki pages: the hero is annotated and etched -Whenever a component page is created or its model changes (here, or through molecule-publish's "create missing component pages" stage), the page's hero GLB follows `eda-component-hero` (adom/eda-engineering): the part with its laser-etched MPN (`adom-aiflow-etch`, colours kept, along the long axis), teal dashed copper-pad outlines from the real pads at alpha 0.50, the footprint silkscreen at 0.30, and pin labels at 0.50, as separate nonphysical nodes in the verified Z-up seating frame with pin 1 and polarity matching the footprint (use its `tools/component-reference-overlay.py`). The plain STEP and the board GLB stay clean (no footprint art); publish the marked STEP as an extra variant with provenance. Check the published hero by looking at it: top, oblique, underside and the pin-1 corner. A plain grey thumbnail as a component hero is not done.+Whenever a component page is created or its model changes (here, or through molecule-publish's "create missing component pages" stage), the page's hero GLB follows `eda-component-hero` (adom/eda-engineering): the part with its laser-etched MPN (`adom-aiflow-etch`, colours kept, along the long axis, or `--along x|y` to keep it off a polarity stripe; a vendor model that already prints its marking is left as it is), teal dashed copper-pad outlines from the real pads at alpha 0.50, the footprint silkscreen at 0.30, and pin labels at 0.50, as separate nonphysical nodes in the verified Z-up seating frame with pin 1 and polarity matching the footprint (use its `tools/component-reference-overlay.py`). The plain STEP and the board GLB stay clean (no footprint art); publish the marked STEP as an extra variant with provenance. Check the published hero by looking at it: top, oblique, underside and the pin-1 corner. A plain grey thumbnail as a component hero is not done.  ## Processing STEP files locally 
tools/laser-etch/etch_part.py+6−2
@@ -17,6 +17,7 @@ HERE = os.path.dirname(os.path.abspath(__file__)); PY = sys.executable ap = argparse.ArgumentParser() ap.add_argument("--in", dest="src", required=True); ap.add_argument("--mpn", required=True); ap.add_argument("--out", required=True) ap.add_argument("--up", default="z", choices=["z", "y"]); ap.add_argument("--frac", default="0.55")+ap.add_argument("--along", default="long", choices=["long", "x", "y"], help="direction the text runs in the stood-up frame: long axis (default), or x/y to keep it off a printed band such as an electrolytic's polarity stripe") ap.add_argument("--mark", default="light"); ap.add_argument("--service", default=os.environ.get("ADOM_CHIP_LASER_URL", "https://laserchip-6dn1tl7vjz5u.adom.cloud")) a = ap.parse_args() from OCP.STEPControl import STEPControl_Reader@@ -32,14 +33,17 @@ tmp = tempfile.mkdtemp(prefix="etch-"); cur = a.src; undo = [] if a.up == "y":                                     # Y-up vendor model: stand it up first     run(os.path.join(HERE, "rotate_step_xcaf.py"), cur, f"{tmp}/up.step", "90", "x"); cur = f"{tmp}/up.step"; undo.append(("-90", "x")) dx, dy, dz = ext(cur)-if dy > dx * 1.02:                                  # long axis along Y: turn it onto X so the mark runs along it+if (a.along == "long" and dy > dx * 1.02) or a.along == "y":   # text should run along Y: turn Y onto X so the service writes along it     run(os.path.join(HERE, "rotate_step_xcaf.py"), cur, f"{tmp}/long.step", "90", "z"); cur = f"{tmp}/long.step"; undo.append(("-90", "z")) req = urllib.request.Request(f"{a.service.rstrip('/')}/etch?mpn={urllib.request.quote(a.mpn)}&frac={a.frac}", data=open(cur, "rb").read(), method="POST") with urllib.request.urlopen(req, timeout=300) as resp:     open(f"{tmp}/etched.step", "wb").write(resp.read()); size = resp.headers.get("X-Etch-Fontsize-Mm") rgb = {"light": "0.78,0.78,0.76", "dark": "0.012,0.012,0.012"}.get(a.mark, a.mark) run(os.path.join(HERE, "merge_etch.py"), cur, f"{tmp}/etched.step", f"{tmp}/merged.step", "0.01", rgb); cur = f"{tmp}/merged.step"+# the service lays the mark at the bbox top: on an MLCC or resistor that is the end caps, so it floats+# above the body; seat it on the surface actually under the text+run(os.path.join(HERE, "seat_mark.py"), cur, f"{tmp}/seated.step", "0.005"); cur = f"{tmp}/seated.step" for i, (deg, ax) in enumerate(reversed(undo)):     nxt = f"{tmp}/undo{i}.step"; run(os.path.join(HERE, "rotate_step_xcaf.py"), cur, nxt, deg, ax); cur = nxt os.replace(cur, a.out)-print(json.dumps({"ok": True, "out": a.out, "mpn": a.mpn, "fontMm": float(size) if size else None, "alongLongAxis": True, "turnedForLongAxis": any(ax == "z" for _, ax in undo), "stoodUp": a.up == "y", "mark": rgb}))+print(json.dumps({"ok": True, "out": a.out, "mpn": a.mpn, "fontMm": float(size) if size else None, "alongLongAxis": a.along == "long", "along": a.along, "turnedForLongAxis": any(ax == "z" for _, ax in undo), "stoodUp": a.up == "y", "mark": rgb}))
tools/laser-etch/seat_mark.pyadded+83
@@ -0,0 +1,83 @@+#!/usr/bin/env python3+"""Seat a flat laser_etch mark on the surface directly beneath it (Z-up STEP): the etch service puts the+mark at the part's bbox top, which floats above a body that is lower than its terminations (MLCC).+Casts vertical rays through the mark footprint, takes the highest surface hit (excluding the mark) and+moves the mark to that height + lift. Colours and names kept (XCAF).+Usage: seat_mark.py in.step out.step [lift_mm=0.005]"""+import sys+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+from OCP.TDF import TDF_ChildIterator, TDF_Label+from OCP.TopLoc import TopLoc_Location+from OCP.TDataStd import TDataStd_Name+from OCP.Bnd import Bnd_Box+from OCP.BRepBndLib import BRepBndLib+from OCP.IntCurvesFace import IntCurvesFace_ShapeIntersector+from OCP.gp import gp_Lin, gp_Pnt, gp_Dir, gp_Trsf, gp_Vec+from OCP.BRepBuilderAPI import BRepBuilderAPI_Transform+from OCP.TopoDS import TopoDS_Compound+from OCP.BRep import BRep_Builder+src, out = sys.argv[1:3]; lift = float(sys.argv[3]) if len(sys.argv) > 3 else 0.005+doc = TDocStd_Document(TCollection_ExtendedString("d"))+r = STEPCAFControl_Reader(); r.SetColorMode(True); r.SetNameMode(True); r.SetLayerMode(True); r.ReadFile(src); r.Transfer(doc)+st = XCAFDoc_DocumentTool.ShapeTool_s(doc.Main())+def name(l):+    nm = TDataStd_Name(); return nm.Get().ToExtString() if l.FindAttribute(TDataStd_Name.GetID_s(), nm) else ''+found = []; others = TopoDS_Compound(); bb = BRep_Builder(); bb.MakeCompound(others)+def walk(l, loc, named):+    if st.IsReference_s(l):+        ref = TDF_Label(); st.GetReferredShape_s(l, ref)+        return walk(ref, loc.Multiplied(st.GetLocation_s(l)), named or name(l) == 'laser_etch')+    if st.IsAssembly_s(l):+        it = TDF_ChildIterator(l, False)+        while it.More(): c = it.Value(); it.Next(); walk(c, loc, named)+        return+    s = st.GetShape_s(l)+    if named or name(l) == 'laser_etch': found.append((l, loc))+    else: bb.Add(others, s.Moved(loc))+it = TDF_ChildIterator(st.Label(), False)+while it.More():+    l = it.Value(); it.Next()+    if st.IsFree_s(l) and st.IsTopLevel(l): walk(l, TopLoc_Location(), False)+assert len(found) == 1, len(found); ml, mloc = found[0]; ms = st.GetShape_s(ml).Moved(mloc)+b = Bnd_Box(); BRepBndLib.Add_s(ms, b); mn, mx = b.CornerMin(), b.CornerMax()+inter = IntCurvesFace_ShapeIntersector(); inter.Load(others, 1e-6)+hits = []+for fx in (0.1, 0.3, 0.5, 0.7, 0.9):+    for fy in (0.25, 0.5, 0.75):+        x = mn.X() + (mx.X()-mn.X())*fx; y = mn.Y() + (mx.Y()-mn.Y())*fy+        inter.Perform(gp_Lin(gp_Pnt(x, y, mx.Z()+5), gp_Dir(0, 0, -1)), -1e9, 1e9)+        zs = [inter.Pnt(k).Z() for k in range(1, inter.NbPnt()+1)]+        if zs: hits.append(max(zs))+top = max(hits); dz = top + lift - mn.Z()+v = gp_Vec(0, 0, dz).Transformed(mloc.Transformation().Inverted())   # world lift expressed in the part's own frame+t = gp_Trsf(); t.SetTranslation(v)+from OCP.XCAFDoc import XCAFDoc_ColorType+from OCP.Quantity import Quantity_Color+from OCP.TopExp import TopExp_Explorer+from OCP.TopAbs import TopAbs_FACE+ct = XCAFDoc_DocumentTool.ColorTool_s(doc.Main()); col = None+for src_ in [st.GetShape_s(ml)]:+    c = Quantity_Color()+    if ct.GetColor_s(ml, XCAFDoc_ColorType.XCAFDoc_ColorSurf, c) or ct.GetColor_s(ml, XCAFDoc_ColorType.XCAFDoc_ColorGen, c): col = c+    ex = TopExp_Explorer(src_, TopAbs_FACE)+    while col is None and ex.More():+        c = Quantity_Color()+        if ct.GetColor(ex.Current(), XCAFDoc_ColorType.XCAFDoc_ColorSurf, c) or ct.GetColor(ex.Current(), XCAFDoc_ColorType.XCAFDoc_ColorGen, c): col = c+        ex.Next()+assert col is not None, 'mark colour not found'+st.SetShape(ml, BRepBuilderAPI_Transform(st.GetShape_s(ml), t, True).Shape())+ct.SetColor(ml, col, XCAFDoc_ColorType.XCAFDoc_ColorSurf); ct.SetColor(ml, col, XCAFDoc_ColorType.XCAFDoc_ColorGen)+ex = TopExp_Explorer(st.GetShape_s(ml), TopAbs_FACE); nf = 0          # face-level colour too, as the etch tool wrote it (some renderers read only that)+while ex.More():+    sub = st.AddSubShape(ml, ex.Current()); ex.Next()+    if not sub.IsNull(): ct.SetColor(sub, col, XCAFDoc_ColorType.XCAFDoc_ColorSurf); nf += 1+print("face colours", nf)+print(f"mark colour kept {col.Red():.3f},{col.Green():.3f},{col.Blue():.3f}")+st.UpdateAssemblies()+w = STEPCAFControl_Writer(); w.SetColorMode(True); w.SetNameMode(True); w.SetLayerMode(True)+w.Transfer(doc, STEPControl_AsIs); w.Write(out)+print(f"surface under mark z {min(hits):.4f}..{top:.4f}; mark z {mn.Z():.4f} -> {mn.Z()+dz:.4f} (moved {dz*1000:+.1f} um)")