← Commit history
README.md+22−14
@@ -1,11 +1,11 @@-# Component hero standard (adom-hero 1.1)+# Component hero standard (adom-hero 1.3)  Every component page on the wiki shows its part in 3D with the footprint drawn under it: the pads as teal dashed outlines, the silkscreen with pin 1, and each pad's signal name. This document defines that artwork exactly, so any person or AI builds the same thing, a linter can check it, and our factory can generate it for every page. -Status: 1.1, 2026-09-30 (1.1 makes the pad outlines bolder; see the change log at the end). Applied to 7 factory pages as a test (linked at the end). The generator is part+Status: 1.3, 2026-09-30 (see the change log at the end). Applied to 7 factory pages as a test (linked at the end). The generator is part of this page's skill (`skills/component-hero/`), so any AI that installs it can build a conforming hero.  ![SOT-23-6 hero: top and oblique](docs/example-sot23-6.png)@@ -93,17 +93,20 @@ uses this teal, and the `wiki-component` skill specifies it.   - Pad rotation is applied, including its sign. - **Dashes:** the outline is dashed as a closed path, and a dash may turn a corner.   - Let **span** be the largest dimension of the pad field, in mm, and at least 1.-  - **dash** = clamp(0.035 × span, 0.18, 0.6) mm.+  - **dash** = clamp(0.025 × span, 0.12, 0.45) mm.   - **gap** = 0.6 × dash.-  - **stroke** = clamp(0.012 × span, 0.05, 0.15) mm.+  - **stroke** = clamp(0.006 × span, 0.03, 0.09) mm.   - **Per-pad cap**, so small pads still read as their shape: dash ≤ max(perimeter / 6, 0.05), and     stroke ≤ max(perimeter / 25, 0.02).-- **Geometry:** each dash piece is a flat quad strip of the stroke width, centred on the path.-- **Worked example:** a SOT-23-6 gives 0.18 mm dashes with a 0.05 mm stroke; an LQFP-48 gives about-  0.32 mm dashes with a 0.11 mm stroke. On 0.3 mm BGA balls the per-pad cap keeps them as dashed circles.-- **Why this size:** the outlines must read before the silkscreen at the wiki viewer's default zoom.-  1.0 used the hand-built heroes' 0.015 mm stroke, which vanished in the viewer, so the 30% silkscreen was-  taken for the footprint.+- **Geometry:** each dash is one continuous ribbon of the stroke width, centred on the path, with mitred+  joins at every bend (mitre limit 4×). There are no gaps at bends and no overlapping triangles: overlaps+  double the alpha and show as bright seams. Curves (rounded corners, circles, ovals, silkscreen arcs) are+  sampled so that no straight piece is longer than 0.005 mm, so they stay smooth when zoomed in.+  Silkscreen strokes use the same ribbons. Meshes are indexed, so shared vertices are stored once.+- **Worked example:** a SOT-23-6 gives 0.12 mm dashes with a 0.03 mm stroke; an LQFP-48 gives about+  0.23 mm dashes with a 0.054 mm stroke. On 0.3 mm BGA balls the per-pad cap keeps them as dashed circles.+- **Why this weight:** subtle, but visible at the wiki viewer's default zoom. The hand-built heroes' 0.015 mm+  stroke vanished there, so the 30% silkscreen was taken for the footprint; 1.1's 0.05 mm read as heavy.  ## Layer 2: silkscreen and pin 1 @@ -156,7 +159,7 @@ The hero's glTF `asset.extras` carries one object. Here is the real block from a  ```json {-  "spec": "adom-hero 1.1",+  "spec": "adom-hero 1.3",   "mpn": "OPA2325IDGKT",   "kind": "component + footprint reference art",   "board_use": false,@@ -164,7 +167,7 @@ The hero's glTF `asset.extras` carries one object. Here is the real block from a   "layers": {     "pad_outlines": { "material": "Footprint pads - teal dashed outlines - 50 percent opacity",                       "rgba": [0, 0.902, 0.863, 0.5], "pads": 8,-                      "span_mm": 4.05, "dash_mm": 0.18, "gap_mm": 0.108, "pad_stroke_mm": 0.0500 },+                      "span_mm": 4.05, "dash_mm": 0.12, "gap_mm": 0.072, "pad_stroke_mm": 0.0300 },     "silkscreen":   { "material": "Silkscreen reference lines - 30 percent opacity",                       "rgba": [0.92, 0.96, 1.0, 0.3], "footprint_items": 3,                       "pin1": { "source": "footprint silkscreen", "pad": "1" } },@@ -254,8 +257,8 @@ To publish, add `<slug>-hero.glb` and `<slug>-hero.overlay.json` to the page, se   `-0.02 mm` plane and the unlit materials. - **Now exact:** dash, gap and stroke sizes, label height and placement, the pin-1 rules, and the   exposed-pad and ball-grid rules.-- **Changed:** the pad outlines are about 3× bolder than on the hand-built heroes (1.1), so the footprint-  reads first in the viewer.+- **Changed:** the pad outlines are about 2× the hand-built heroes' stroke, so the footprint is visible at+  the viewer's default zoom, and each dash is one smooth continuous stroke. - **Added:**   - the pin-1 dot on the silkscreen layer. John asked for it, and the one earlier attempt was a 3D block     stuck on the chip;@@ -300,6 +303,11 @@ Hand-built style A, for comparison:  ## Change log +- **1.3 (2026-09-30):** each dash is one continuous mitred ribbon, curves sampled to 0.005 mm, meshes+  indexed. Ray, zoomed in: "why is it not a continuous shape? and smooth?" The dashes were separate quads+  per segment, with wedge gaps and bright overlaps at rounded corners, and corners had 7 points per 90°.+- **1.2 (2026-09-30):** outline weight between 1.0 and 1.1 (stroke 0.03-0.09 mm, dash 0.12-0.45 mm).+  Colby liked the subtle look; 1.0 vanished at default zoom. Ray chose the middle. - **1.1 (2026-09-30):** pad outlines bolder: stroke 0.015 to 0.05-0.15 mm, dash 0.07 to 0.18-0.6 mm. Colby   asked whether the gray lines were the footprint: at 1.0 sizes the teal outlines vanished at the viewer's   default zoom and only the silkscreen showed.
SKILL.md+1−1
@@ -1,6 +1,6 @@ --- name: component-hero-description: Build a component page's annotated 3D hero to the Adom standard (adom-hero 1.1) - the physical chip plus teal dashed pad outlines (50% opacity), silkscreen with pin 1 (70% transparent), and each pad's signal name - as a separate <slug>-hero.glb, with the board-use GLB left clean. Deterministic generator included (hero_glb.py, no model calls). Use when publishing or updating any component page's 3D model, when a page shows the bare chip without its footprint, or when asked for footprint outlines, pad labels, pin 1 or silkscreen in the 3D view. Trigger words - component hero, hero glb, footprint outline in 3d, teal dashed pads, pad outlines, silkscreen in the glb, pin 1 on silk, signal names on pads, pad labels 3d, model_3d, model_3d_plain, adomHero, adom-hero 1.1.+description: Build a component page's annotated 3D hero to the Adom standard (adom-hero 1.3) - the physical chip plus teal dashed pad outlines (50% opacity), silkscreen with pin 1 (70% transparent), and each pad's signal name - as a separate <slug>-hero.glb, with the board-use GLB left clean. Deterministic generator included (hero_glb.py, no model calls). Use when publishing or updating any component page's 3D model, when a page shows the bare chip without its footprint, or when asked for footprint outlines, pad labels, pin 1 or silkscreen in the 3D view. Trigger words - component hero, hero glb, footprint outline in 3d, teal dashed pads, pad outlines, silkscreen in the glb, pin 1 on silk, signal names on pads, pad labels 3d, model_3d, model_3d_plain, adomHero, adom-hero 1.3. ---  # component-hero
docs/example-qfn36.png
⋯ 1 unchanged line ⋯
docs/example-sot23-6.png
⋯ 1 unchanged line ⋯
skills/component-hero/SKILL.md+1−1
@@ -1,6 +1,6 @@ --- name: component-hero-description: Build a component page's annotated 3D hero to the Adom standard (adom-hero 1.1) - the physical chip plus teal dashed pad outlines (50% opacity), silkscreen with pin 1 (70% transparent), and each pad's signal name - as a separate <slug>-hero.glb, with the board-use GLB left clean. Deterministic generator included (hero_glb.py, no model calls). Use when publishing or updating any component page's 3D model, when a page shows the bare chip without its footprint, or when asked for footprint outlines, pad labels, pin 1 or silkscreen in the 3D view. Trigger words - component hero, hero glb, footprint outline in 3d, teal dashed pads, pad outlines, silkscreen in the glb, pin 1 on silk, signal names on pads, pad labels 3d, model_3d, model_3d_plain, adomHero, adom-hero 1.1.+description: Build a component page's annotated 3D hero to the Adom standard (adom-hero 1.3) - the physical chip plus teal dashed pad outlines (50% opacity), silkscreen with pin 1 (70% transparent), and each pad's signal name - as a separate <slug>-hero.glb, with the board-use GLB left clean. Deterministic generator included (hero_glb.py, no model calls). Use when publishing or updating any component page's 3D model, when a page shows the bare chip without its footprint, or when asked for footprint outlines, pad labels, pin 1 or silkscreen in the 3D view. Trigger words - component hero, hero glb, footprint outline in 3d, teal dashed pads, pad outlines, silkscreen in the glb, pin 1 on silk, signal names on pads, pad labels 3d, model_3d, model_3d_plain, adomHero, adom-hero 1.3. ---  # component-hero
skills/component-hero/reference/standard.md+22−14
@@ -1,11 +1,11 @@-# Component hero standard (adom-hero 1.1)+# Component hero standard (adom-hero 1.3)  Every component page on the wiki shows its part in 3D with the footprint drawn under it: the pads as teal dashed outlines, the silkscreen with pin 1, and each pad's signal name. This document defines that artwork exactly, so any person or AI builds the same thing, a linter can check it, and our factory can generate it for every page. -Status: 1.1, 2026-09-30 (1.1 makes the pad outlines bolder; see the change log at the end). Applied to 7 factory pages as a test (linked at the end). The generator is part+Status: 1.3, 2026-09-30 (see the change log at the end). Applied to 7 factory pages as a test (linked at the end). The generator is part of this page's skill (`skills/component-hero/`), so any AI that installs it can build a conforming hero.  ![SOT-23-6 hero: top and oblique](docs/example-sot23-6.png)@@ -93,17 +93,20 @@ uses this teal, and the `wiki-component` skill specifies it.   - Pad rotation is applied, including its sign. - **Dashes:** the outline is dashed as a closed path, and a dash may turn a corner.   - Let **span** be the largest dimension of the pad field, in mm, and at least 1.-  - **dash** = clamp(0.035 × span, 0.18, 0.6) mm.+  - **dash** = clamp(0.025 × span, 0.12, 0.45) mm.   - **gap** = 0.6 × dash.-  - **stroke** = clamp(0.012 × span, 0.05, 0.15) mm.+  - **stroke** = clamp(0.006 × span, 0.03, 0.09) mm.   - **Per-pad cap**, so small pads still read as their shape: dash ≤ max(perimeter / 6, 0.05), and     stroke ≤ max(perimeter / 25, 0.02).-- **Geometry:** each dash piece is a flat quad strip of the stroke width, centred on the path.-- **Worked example:** a SOT-23-6 gives 0.18 mm dashes with a 0.05 mm stroke; an LQFP-48 gives about-  0.32 mm dashes with a 0.11 mm stroke. On 0.3 mm BGA balls the per-pad cap keeps them as dashed circles.-- **Why this size:** the outlines must read before the silkscreen at the wiki viewer's default zoom.-  1.0 used the hand-built heroes' 0.015 mm stroke, which vanished in the viewer, so the 30% silkscreen was-  taken for the footprint.+- **Geometry:** each dash is one continuous ribbon of the stroke width, centred on the path, with mitred+  joins at every bend (mitre limit 4×). There are no gaps at bends and no overlapping triangles: overlaps+  double the alpha and show as bright seams. Curves (rounded corners, circles, ovals, silkscreen arcs) are+  sampled so that no straight piece is longer than 0.005 mm, so they stay smooth when zoomed in.+  Silkscreen strokes use the same ribbons. Meshes are indexed, so shared vertices are stored once.+- **Worked example:** a SOT-23-6 gives 0.12 mm dashes with a 0.03 mm stroke; an LQFP-48 gives about+  0.23 mm dashes with a 0.054 mm stroke. On 0.3 mm BGA balls the per-pad cap keeps them as dashed circles.+- **Why this weight:** subtle, but visible at the wiki viewer's default zoom. The hand-built heroes' 0.015 mm+  stroke vanished there, so the 30% silkscreen was taken for the footprint; 1.1's 0.05 mm read as heavy.  ## Layer 2: silkscreen and pin 1 @@ -156,7 +159,7 @@ The hero's glTF `asset.extras` carries one object. Here is the real block from a  ```json {-  "spec": "adom-hero 1.1",+  "spec": "adom-hero 1.3",   "mpn": "OPA2325IDGKT",   "kind": "component + footprint reference art",   "board_use": false,@@ -164,7 +167,7 @@ The hero's glTF `asset.extras` carries one object. Here is the real block from a   "layers": {     "pad_outlines": { "material": "Footprint pads - teal dashed outlines - 50 percent opacity",                       "rgba": [0, 0.902, 0.863, 0.5], "pads": 8,-                      "span_mm": 4.05, "dash_mm": 0.18, "gap_mm": 0.108, "pad_stroke_mm": 0.0500 },+                      "span_mm": 4.05, "dash_mm": 0.12, "gap_mm": 0.072, "pad_stroke_mm": 0.0300 },     "silkscreen":   { "material": "Silkscreen reference lines - 30 percent opacity",                       "rgba": [0.92, 0.96, 1.0, 0.3], "footprint_items": 3,                       "pin1": { "source": "footprint silkscreen", "pad": "1" } },@@ -254,8 +257,8 @@ To publish, add `<slug>-hero.glb` and `<slug>-hero.overlay.json` to the page, se   `-0.02 mm` plane and the unlit materials. - **Now exact:** dash, gap and stroke sizes, label height and placement, the pin-1 rules, and the   exposed-pad and ball-grid rules.-- **Changed:** the pad outlines are about 3× bolder than on the hand-built heroes (1.1), so the footprint-  reads first in the viewer.+- **Changed:** the pad outlines are about 2× the hand-built heroes' stroke, so the footprint is visible at+  the viewer's default zoom, and each dash is one smooth continuous stroke. - **Added:**   - the pin-1 dot on the silkscreen layer. John asked for it, and the one earlier attempt was a 3D block     stuck on the chip;@@ -300,6 +303,11 @@ Hand-built style A, for comparison:  ## Change log +- **1.3 (2026-09-30):** each dash is one continuous mitred ribbon, curves sampled to 0.005 mm, meshes+  indexed. Ray, zoomed in: "why is it not a continuous shape? and smooth?" The dashes were separate quads+  per segment, with wedge gaps and bright overlaps at rounded corners, and corners had 7 points per 90°.+- **1.2 (2026-09-30):** outline weight between 1.0 and 1.1 (stroke 0.03-0.09 mm, dash 0.12-0.45 mm).+  Colby liked the subtle look; 1.0 vanished at default zoom. Ray chose the middle. - **1.1 (2026-09-30):** pad outlines bolder: stroke 0.015 to 0.05-0.15 mm, dash 0.07 to 0.18-0.6 mm. Colby   asked whether the gray lines were the footprint: at 1.0 sizes the teal outlines vanished at the viewer's   default zoom and only the silkscreen showed.
skills/component-hero/scripts/hero_glb.py+46−17
@@ -27,7 +27,7 @@ import argparse, hashlib, json, math, os, re, struct, sys import numpy as np from pygltflib import GLTF2, Accessor, BufferView, Material, Mesh, Node, Primitive, Attributes, PbrMetallicRoughness -SPEC = 'adom-hero 1.1'+SPEC = 'adom-hero 1.3' Z = -0.02                                   # mm, artwork plane PAD_MAT = ('Footprint pads - teal dashed outlines - 50 percent opacity', [0.0, 0.902, 0.863, 0.5]) SILK_MAT = ('Silkscreen reference lines - 30 percent opacity', [0.92, 0.96, 1.0, 0.3])@@ -104,7 +104,7 @@ def parse_footprint(path):         elif g[0] == 'fp_circle':             (cx, cy), (ex, ey) = xy(child(g, 'center')), xy(child(g, 'end'))             r = math.hypot(ex - cx, ey - cy)-            poly = [(cx + r * math.cos(t), cy + r * math.sin(t)) for t in np.linspace(0, 2 * math.pi, 49)]+            poly = [(cx + r * math.cos(t), cy + r * math.sin(t)) for t in np.linspace(0, 2 * math.pi, arc_n(r, 360) + 1)]             silk.append(('fill' if filled else 'path', poly, w))         elif g[0] == 'fp_arc':             if child(g, 'mid'):@@ -125,7 +125,7 @@ def parse_footprint(path):     return pads, silk, mdl  -def arc3(a, m, b, n=24):+def arc3(a, m, b, n=None):     ax, ay = a; bx, by = m; cx, cy = b     d = 2 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by))     if abs(d) < 1e-12: return [a, b]@@ -135,19 +135,25 @@ def arc3(a, m, b, n=24):     t0, tm, t1 = (math.atan2(p[1] - uy, p[0] - ux) for p in (a, m, b))     def norm(t): return (t - t0) % (2 * math.pi)     sweep = norm(t1) if norm(tm) < norm(t1) else norm(t1) - 2 * math.pi+    n = n or arc_n(r, math.degrees(sweep))     return [(ux + r * math.cos(t0 + sweep * k / n), uy + r * math.sin(t0 + sweep * k / n)) for k in range(n + 1)]   # ---------------------------------------------------------------- geometry+def arc_n(r, sweep_deg, chord=0.005):+    """Segments for an arc so no straight piece is longer than `chord` mm (smooth at any zoom)."""+    return int(min(max(math.ceil(abs(math.radians(sweep_deg)) * r / chord), 8), 240))++ def pad_outline(p):     """Closed polyline of the copper perimeter in footprint coordinates."""     w, h = p['w'], p['h']     if p['shape'] == 'circle':-        loc = [(w / 2 * math.cos(t), w / 2 * math.sin(t)) for t in np.linspace(0, 2 * math.pi, 49)]+        n = arc_n(w / 2, 360); loc = [(w / 2 * math.cos(t), w / 2 * math.sin(t)) for t in np.linspace(0, 2 * math.pi, n + 1)]     elif p['shape'] == 'oval':         r = min(w, h) / 2; L = (max(w, h) - 2 * r) / 2; loc = []         for cx, a0 in ((L, -90), (-L, 90)):-            loc += [(cx + r * math.cos(math.radians(a0 + k * 7.5)), r * math.sin(math.radians(a0 + k * 7.5))) for k in range(25)]+            n = arc_n(r, 180); loc += [(cx + r * math.cos(math.radians(a0 + k * 180 / n)), r * math.sin(math.radians(a0 + k * 180 / n))) for k in range(n + 1)]         if h > w: loc = [(y, x) for x, y in loc]         loc.append(loc[0])     elif p['shape'] in ('roundrect', 'rect', 'trapezoid', 'custom'):@@ -159,7 +165,7 @@ def pad_outline(p):         else:             loc = []             for cx, cy, a0 in ((w / 2 - r, -h / 2 + r, -90), (w / 2 - r, h / 2 - r, 0), (-w / 2 + r, h / 2 - r, 90), (-w / 2 + r, -h / 2 + r, 180)):-                loc += [(cx + r * math.cos(math.radians(a0 + k * 15)), cy + r * math.sin(math.radians(a0 + k * 15))) for k in range(7)]+                n = arc_n(r, 90); loc += [(cx + r * math.cos(math.radians(a0 + k * 90 / n)), cy + r * math.sin(math.radians(a0 + k * 90 / n))) for k in range(n + 1)]             loc.append(loc[0])     else:         loc = [(-w / 2, -h / 2), (w / 2, -h / 2), (w / 2, h / 2), (-w / 2, h / 2), (-w / 2, -h / 2)]@@ -169,14 +175,34 @@ def pad_outline(p):   def stroke_quads(poly, width):-    """Triangles for a stroked open polyline (butt ends, mitre-free: each segment its own quad)."""+    """One continuous ribbon along a polyline: offset both sides with mitred joins, so bends have no gaps+    and no overlapping triangles (overlaps double the alpha and show as bright seams)."""+    pts = [poly[0]] + [q for a, q in zip(poly, poly[1:]) if math.hypot(q[0] - a[0], q[1] - a[1]) > 1e-9]+    if len(pts) < 2: return []+    closed = len(pts) > 2 and math.hypot(pts[0][0] - pts[-1][0], pts[0][1] - pts[-1][1]) < 1e-9+    if closed: pts = pts[:-1]+    n = len(pts); hw = width / 2+    def seg_normal(i, j):+        dx, dy = pts[j][0] - pts[i][0], pts[j][1] - pts[i][1]; L = math.hypot(dx, dy)+        return (-dy / L, dx / L)+    left, right = [], []+    for i in range(n):+        prv = (i - 1) % n if (closed or i > 0) else None+        nxt = (i + 1) % n if (closed or i < n - 1) else None+        if prv is None: nx, ny = seg_normal(i, nxt)+        elif nxt is None: nx, ny = seg_normal(prv, i)+        else:+            a, b = seg_normal(prv, i), seg_normal(i, nxt)+            mx, my = a[0] + b[0], a[1] + b[1]; ml = math.hypot(mx, my)+            if ml < 1e-6: nx, ny = b+            else:+                mx, my = mx / ml, my / ml; cosh = max(mx * b[0] + my * b[1], 0.25)   # mitre limit 4x+                nx, ny = mx / cosh, my / cosh+        left.append((pts[i][0] + nx * hw, pts[i][1] + ny * hw)); right.append((pts[i][0] - nx * hw, pts[i][1] - ny * hw))+    idx = list(range(n)) + ([0] if closed else [])     tris = []-    for (x1, y1), (x2, y2) in zip(poly, poly[1:]):-        dx, dy = x2 - x1, y2 - y1; L = math.hypot(dx, dy)-        if L < 1e-9: continue-        nx, ny = -dy / L * width / 2, dx / L * width / 2-        a, b, c, d = (x1 + nx, y1 + ny), (x2 + nx, y2 + ny), (x2 - nx, y2 - ny), (x1 - nx, y1 - ny)-        tris += [(a, b, c), (a, c, d)]+    for i, j in zip(idx, idx[1:]):+        tris += [(left[i], left[j], right[j]), (left[i], right[j], right[i])]     return tris  @@ -258,9 +284,10 @@ def scale_params(pads):     span = max(max(xs) - min(xs) + max(p['w'] for p in pads), max(ys) - min(ys) + max(p['h'] for p in pads), 1.0)     # 1.1 (2026-09-30): bolder. At 0.015 mm the outlines vanished at the wiki viewer's default zoom and the     # 30% silkscreen read as "the footprint" (Colby). The footprint must read first.-    dash = min(max(0.035 * span, 0.18), 0.6)+    # 1.2: between the two -- 1.1 read as heavy to Colby ("I like that it's subtle"), 1.0 vanished (Ray chose the middle)+    dash = min(max(0.025 * span, 0.12), 0.45)     return dict(span_mm=round(span, 3), dash_mm=round(dash, 4), gap_mm=round(0.6 * dash, 4),-                pad_stroke_mm=round(min(max(0.012 * span, 0.05), 0.15), 4))+                pad_stroke_mm=round(min(max(0.006 * span, 0.03), 0.09), 4))   def is_grid(pads):@@ -370,9 +397,11 @@ def pin1_dot(pads, names, silk=()):  # ---------------------------------------------------------------- GLB assembly def tris_to_arrays(tris):+    """Indexed mesh: shared vertices stored once (the ribbons and glyphs share most corners), ~3x smaller."""     P = np.array([[x / 1000.0, -y / 1000.0, Z / 1000.0] for t in tris for (x, y) in t], dtype=np.float32)-    I = np.arange(len(P), dtype=np.uint32)-    return P, I+    key = np.round(P.astype(np.float64) * 1e7).astype(np.int64)+    _, first, inv = np.unique(key, axis=0, return_index=True, return_inverse=True)+    return P[first], inv.reshape(-1).astype(np.uint32)   def add_mesh(g, blob, name, mat_idx, tris):